diff --git a/packages/forms/docs/10-rich-editor.md b/packages/forms/docs/10-rich-editor.md index 0f912b8d77..ce1d9a0220 100644 --- a/packages/forms/docs/10-rich-editor.md +++ b/packages/forms/docs/10-rich-editor.md @@ -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>> + */ + public function getEnabledToolbarButtons(): array + { + return ['highlight', 'highlightWithCustomColor']; + } + + /** + * @return array + */ + 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). diff --git a/packages/forms/src/Components/Concerns/InteractsWithToolbarButtons.php b/packages/forms/src/Components/Concerns/InteractsWithToolbarButtons.php index 434823cde6..361d749b63 100644 --- a/packages/forms/src/Components/Concerns/InteractsWithToolbarButtons.php +++ b/packages/forms/src/Components/Concerns/InteractsWithToolbarButtons.php @@ -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>>}> + */ + protected function getExtraToolbarButtonsModifications(): array + { + return []; + } + /** * @return array> */ diff --git a/packages/forms/src/Components/RichEditor.php b/packages/forms/src/Components/RichEditor.php index e0782b8658..d804440cbb 100644 --- a/packages/forms/src/Components/RichEditor.php +++ b/packages/forms/src/Components/RichEditor.php @@ -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>>}> + */ + 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> */ diff --git a/packages/forms/src/Components/RichEditor/Plugins/Contracts/HasToolbarButtons.php b/packages/forms/src/Components/RichEditor/Plugins/Contracts/HasToolbarButtons.php new file mode 100644 index 0000000000..ed0694238e --- /dev/null +++ b/packages/forms/src/Components/RichEditor/Plugins/Contracts/HasToolbarButtons.php @@ -0,0 +1,16 @@ +>> + */ + public function getEnabledToolbarButtons(): array; + + /** + * @return array + */ + public function getDisabledToolbarButtons(): array; +} diff --git a/tests/src/Fixtures/RichEditor/TestRichContentPlugin.php b/tests/src/Fixtures/RichEditor/TestRichContentPlugin.php new file mode 100644 index 0000000000..3eed260df2 --- /dev/null +++ b/tests/src/Fixtures/RichEditor/TestRichContentPlugin.php @@ -0,0 +1,69 @@ +>> $enabledButtons + * @param array $disabledButtons + */ + public function __construct( + protected array $enabledButtons = [], + protected array $disabledButtons = [], + ) {} + + /** + * @return array + */ + public function getTipTapPhpExtensions(): array + { + return []; + } + + /** + * @return array + */ + public function getTipTapJsExtensions(): array + { + return []; + } + + /** + * @return array + */ + public function getEditorTools(): array + { + return []; + } + + /** + * @return array + */ + public function getEditorActions(): array + { + return []; + } + + /** + * @return array>> + */ + public function getEnabledToolbarButtons(): array + { + return $this->enabledButtons; + } + + /** + * @return array + */ + public function getDisabledToolbarButtons(): array + { + return $this->disabledButtons; + } +} diff --git a/tests/src/Fixtures/RichEditor/TestRichContentPluginWithoutToolbarButtons.php b/tests/src/Fixtures/RichEditor/TestRichContentPluginWithoutToolbarButtons.php new file mode 100644 index 0000000000..b758b479f7 --- /dev/null +++ b/tests/src/Fixtures/RichEditor/TestRichContentPluginWithoutToolbarButtons.php @@ -0,0 +1,43 @@ + + */ + public function getTipTapPhpExtensions(): array + { + return []; + } + + /** + * @return array + */ + public function getTipTapJsExtensions(): array + { + return []; + } + + /** + * @return array + */ + public function getEditorTools(): array + { + return []; + } + + /** + * @return array + */ + public function getEditorActions(): array + { + return []; + } +} diff --git a/tests/src/Forms/Components/RichEditorTest.php b/tests/src/Forms/Components/RichEditorTest.php index 70455c1ac0..f6a0c28433 100644 --- a/tests/src/Forms/Components/RichEditorTest.php +++ b/tests/src/Forms/Components/RichEditorTest.php @@ -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;