mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
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:
co-authored by
ralphjsmit
Dan Harrin
parent
3f8d4e0b3d
commit
3a3db66ba3
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user