From 57e37e20512e897196876ddcd4c498b0e518ae3e Mon Sep 17 00:00:00 2001 From: RenHai Wen <70972819+People-Sea@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:58:25 +0800 Subject: [PATCH] fix: prevent duplicate toolbar buttons when rich editor plugin enables already-present buttons (#19666) * feat: implement applyEnableToolbarButtonsModification for dynamic button enabling * fix: remove commented-out code for enabling toolbar buttons * test: add tests for toolbar button duplication in RichEditor plugin * fix: improve toolbar button detection logic in RichEditor plugin --- .../Concerns/InteractsWithToolbarButtons.php | 83 ++++++++++++++++++- packages/forms/src/Components/RichEditor.php | 9 ++ tests/src/Forms/Components/RichEditorTest.php | 42 ++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/packages/forms/src/Components/Concerns/InteractsWithToolbarButtons.php b/packages/forms/src/Components/Concerns/InteractsWithToolbarButtons.php index a81522b55e..39b0329645 100644 --- a/packages/forms/src/Components/Concerns/InteractsWithToolbarButtons.php +++ b/packages/forms/src/Components/Concerns/InteractsWithToolbarButtons.php @@ -87,7 +87,7 @@ trait InteractsWithToolbarButtons $buttons = match ($modification['type']) { 'disableAll' => [], 'disable' => $this->applyDisableToolbarButtonsModification($buttons, $modification['buttons']), - 'enable' => [...$buttons, ...$modification['buttons']], + 'enable' => $this->applyEnableToolbarButtonsModification($buttons, $modification['buttons']), default => throw new Exception('Unknown toolbar buttons modification type: [' . $modification['type'] . '].'), }; } @@ -170,6 +170,87 @@ trait InteractsWithToolbarButtons return $modified; } + /** + * @param array> $buttons + * @param array> $buttonsToEnable + * @return array> + */ + protected function applyEnableToolbarButtonsModification(array $buttons, array $buttonsToEnable): array + { + $modified = $buttons; + + foreach ($buttonsToEnable as $button) { + if (is_object($button)) { + $modified[] = $button; + + continue; + } + + if (is_array($button)) { + $filteredGroup = []; + + foreach ($button as $item) { + if (is_object($item)) { + $filteredGroup[] = $item; + + continue; + } + + if ($this->hasToolbarButtonInArray($modified, $item) || in_array($item, $filteredGroup)) { + continue; + } + + $filteredGroup[] = $item; + } + + if (filled($filteredGroup)) { + $modified[] = $filteredGroup; + } + + continue; + } + + if ($this->hasToolbarButtonInArray($modified, $button)) { + continue; + } + + $modified[] = $button; + } + + return $modified; + } + + /** + * @param array> $buttons + */ + protected function hasToolbarButtonInArray(array $buttons, string $button): bool + { + foreach ($buttons as $item) { + if (is_array($item)) { + if ($this->hasToolbarButtonInArray($item, $button)) { + return true; + } + + continue; + } + + if (is_string($item) && ($item === $button)) { + return true; + } + + if (is_object($item) && $this->isToolbarButtonInObject($item, $button)) { + return true; + } + } + + return false; + } + + protected function isToolbarButtonInObject(object $item, string $button): bool + { + return false; + } + /** * @return array>>}> */ diff --git a/packages/forms/src/Components/RichEditor.php b/packages/forms/src/Components/RichEditor.php index 454eb97d7d..116ea86551 100644 --- a/packages/forms/src/Components/RichEditor.php +++ b/packages/forms/src/Components/RichEditor.php @@ -828,6 +828,15 @@ class RichEditor extends Field implements Contracts\CanBeLengthConstrained return $modifications; } + protected function isToolbarButtonInObject(object $item, string $button): bool + { + if ($item instanceof ToolbarButtonGroup) { + return in_array($button, $item->getButtons()); + } + + return false; + } + /** * @return array> */ diff --git a/tests/src/Forms/Components/RichEditorTest.php b/tests/src/Forms/Components/RichEditorTest.php index 1fc3d85505..82a55849c4 100644 --- a/tests/src/Forms/Components/RichEditorTest.php +++ b/tests/src/Forms/Components/RichEditorTest.php @@ -432,6 +432,48 @@ describe('plugins', function (): void { ->toContain('bold'); }); + test('plugin implementing `HasToolbarButtons` does not duplicate toolbar buttons already returned by `toolbarButtons()`', function (): void { + $buttons = Schema::make(Livewire::make()) + ->statePath('data') + ->components([ + RichEditor::make('content') + ->toolbarButtons([ + ['bold', 'highlight'], + ['undo', 'redo'], + ]) + ->plugins([new TestRichContentPlugin(enabledButtons: ['highlight'])]), + ]) + ->getComponents()[0] + ->getToolbarButtons(); + + $flatButtons = array_merge(...$buttons); + $highlightButtons = array_values(array_filter($flatButtons, fn (mixed $button): bool => $button === 'highlight')); + + expect($buttons) + ->toHaveCount(2) + ->and($buttons[0])->toEqual(['bold', 'highlight']) + ->and($buttons[1])->toEqual(['undo', 'redo']) + ->and($highlightButtons)->toHaveCount(1); + }); + + test('plugin implementing `HasToolbarButtons` does not duplicate toolbar buttons already enabled by user `enableToolbarButtons()`', function (): void { + $schema = Schema::make(Livewire::make()) + ->statePath('data') + ->components([ + RichEditor::make('content') + ->plugins([new TestRichContentPlugin(enabledButtons: ['highlight'])]), + ]); + + $richEditor = $schema->getComponents()[0]; + $richEditor->enableToolbarButtons(['highlight']); + + $flatButtons = array_merge(...$richEditor->getToolbarButtons()); + $highlightButtons = array_values(array_filter($flatButtons, fn (mixed $button): bool => $button === 'highlight')); + + expect($highlightButtons) + ->toHaveCount(1); + }); + test('plugin implementing `HasToolbarButtons` can disable toolbar buttons', function (): void { $richEditor = Schema::make(Livewire::make()) ->statePath('data')