Files
filament/packages/forms/src/Components/Builder.php
T

922 lines
26 KiB
PHP

<?php
namespace Filament\Forms\Components;
use Closure;
use Filament\Forms\ComponentContainer;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Builder\Block;
use Filament\Support\Enums\ActionSize;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Facades\FilamentIcon;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use function Filament\Forms\array_move_after;
use function Filament\Forms\array_move_before;
class Builder extends Field implements Contracts\CanConcealComponents, Contracts\HasExtraItemActions
{
use Concerns\CanBeCloned;
use Concerns\CanBeCollapsed;
use Concerns\CanGenerateUuids;
use Concerns\CanLimitItemsLength;
use Concerns\HasExtraItemActions;
/**
* @var view-string
*/
protected string $view = 'filament-forms::components.builder';
protected string | Closure | null $addBetweenActionLabel = null;
protected string | Closure | null $addActionLabel = null;
protected bool | Closure $isReorderable = true;
protected bool | Closure $isReorderableWithDragAndDrop = true;
protected bool | Closure $isReorderableWithButtons = false;
protected bool | Closure $isAddable = true;
protected bool | Closure $isDeletable = true;
protected bool | Closure $hasBlockLabels = true;
protected bool | Closure $hasBlockNumbers = true;
protected ?Closure $modifyAddActionUsing = null;
protected ?Closure $modifyAddBetweenActionUsing = null;
protected ?Closure $modifyCloneActionUsing = null;
protected ?Closure $modifyDeleteActionUsing = null;
protected ?Closure $modifyMoveDownActionUsing = null;
protected ?Closure $modifyMoveUpActionUsing = null;
protected ?Closure $modifyReorderActionUsing = null;
protected ?Closure $modifyCollapseActionUsing = null;
protected ?Closure $modifyExpandActionUsing = null;
protected ?Closure $modifyCollapseAllActionUsing = null;
protected ?Closure $modifyExpandAllActionUsing = null;
protected string | Closure | null $labelBetweenItems = null;
protected bool | Closure $isBlockLabelTruncated = true;
/**
* @var array<string, int | string | null> | null
*/
protected ?array $blockPickerColumns = [];
protected MaxWidth | string | Closure | null $blockPickerWidth = null;
protected function setUp(): void
{
parent::setUp();
$this->default([]);
$this->afterStateHydrated(static function (Builder $component, ?array $state): void {
$items = [];
foreach ($state ?? [] as $itemData) {
$items[$component->generateUuid()] = $itemData;
}
$component->state($items);
});
$this->registerActions([
fn (Builder $component): Action => $component->getAddAction(),
fn (Builder $component): Action => $component->getAddBetweenAction(),
fn (Builder $component): Action => $component->getCloneAction(),
fn (Builder $component): Action => $component->getCollapseAction(),
fn (Builder $component): Action => $component->getCollapseAllAction(),
fn (Builder $component): Action => $component->getDeleteAction(),
fn (Builder $component): Action => $component->getExpandAction(),
fn (Builder $component): Action => $component->getExpandAllAction(),
fn (Builder $component): Action => $component->getMoveDownAction(),
fn (Builder $component): Action => $component->getMoveUpAction(),
fn (Builder $component): Action => $component->getReorderAction(),
]);
$this->mutateDehydratedStateUsing(static function (?array $state): array {
return array_values($state ?? []);
});
}
/**
* @param array<Block> | Closure $blocks
*/
public function blocks(array | Closure $blocks): static
{
$this->childComponents($blocks);
return $this;
}
public function getAddAction(): Action
{
$action = Action::make($this->getAddActionName())
->label(fn (Builder $component) => $component->getAddActionLabel())
->color('gray')
->action(function (array $arguments, Builder $component): void {
$newUuid = $component->generateUuid();
$items = $component->getState();
$items[$newUuid] = [
'type' => $arguments['block'],
'data' => [],
];
$component->state($items);
$component->getChildComponentContainer($newUuid)->fill();
$component->collapsed(false, shouldMakeComponentCollapsible: false);
$component->callAfterStateUpdated();
})
->livewireClickHandlerEnabled(false)
->button()
->size(ActionSize::Small)
->visible(fn (Builder $component): bool => $component->isAddable());
if ($this->modifyAddActionUsing) {
$action = $this->evaluate($this->modifyAddActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function addAction(?Closure $callback): static
{
$this->modifyAddActionUsing = $callback;
return $this;
}
public function getAddActionName(): string
{
return 'add';
}
public function getAddBetweenAction(): Action
{
$action = Action::make($this->getAddBetweenActionName())
->label(fn (Builder $component) => $component->getAddBetweenActionLabel())
->color('gray')
->action(function (array $arguments, Builder $component): void {
$newUuid = $component->generateUuid();
$items = [];
foreach ($component->getState() ?? [] as $uuid => $item) {
$items[$uuid] = $item;
if ($uuid === $arguments['afterItem']) {
$items[$newUuid] = [
'type' => $arguments['block'],
'data' => [],
];
}
}
$component->state($items);
$component->getChildComponentContainer($newUuid)->fill();
$component->collapsed(false, shouldMakeComponentCollapsible: false);
$component->callAfterStateUpdated();
})
->livewireClickHandlerEnabled(false)
->button()
->size(ActionSize::Small)
->visible(fn (Builder $component): bool => $component->isAddable());
if ($this->modifyAddBetweenActionUsing) {
$action = $this->evaluate($this->modifyAddBetweenActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function addBetweenAction(?Closure $callback): static
{
$this->modifyAddBetweenActionUsing = $callback;
return $this;
}
public function getAddBetweenActionName(): string
{
return 'addBetween';
}
public function getCloneAction(): Action
{
$action = Action::make($this->getCloneActionName())
->label(__('filament-forms::components.builder.actions.clone.label'))
->icon(FilamentIcon::resolve('forms::components.builder.actions.clone') ?? 'heroicon-m-square-2-stack')
->color('gray')
->action(function (array $arguments, Builder $component): void {
$newUuid = $component->generateUuid();
$items = $component->getState();
$items[$newUuid] = $items[$arguments['item']];
$component->state($items);
$component->collapsed(false, shouldMakeComponentCollapsible: false);
$component->callAfterStateUpdated();
})
->iconButton()
->size(ActionSize::Small)
->visible(fn (Builder $component): bool => $component->isCloneable());
if ($this->modifyCloneActionUsing) {
$action = $this->evaluate($this->modifyCloneActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function cloneAction(?Closure $callback): static
{
$this->modifyCloneActionUsing = $callback;
return $this;
}
public function getCloneActionName(): string
{
return 'clone';
}
public function getDeleteAction(): Action
{
$action = Action::make($this->getDeleteActionName())
->label(__('filament-forms::components.builder.actions.delete.label'))
->icon(FilamentIcon::resolve('forms::components.builder.actions.delete') ?? 'heroicon-m-trash')
->color('danger')
->action(function (array $arguments, Builder $component): void {
$items = $component->getState();
unset($items[$arguments['item']]);
$component->state($items);
$component->callAfterStateUpdated();
})
->iconButton()
->size(ActionSize::Small)
->visible(fn (Builder $component): bool => $component->isDeletable());
if ($this->modifyDeleteActionUsing) {
$action = $this->evaluate($this->modifyDeleteActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function deleteAction(?Closure $callback): static
{
$this->modifyDeleteActionUsing = $callback;
return $this;
}
public function getDeleteActionName(): string
{
return 'delete';
}
public function getMoveDownAction(): Action
{
$action = Action::make($this->getMoveDownActionName())
->label(__('filament-forms::components.builder.actions.move_down.label'))
->icon(FilamentIcon::resolve('forms::components.builder.actions.move-down') ?? 'heroicon-m-arrow-down')
->color('gray')
->action(function (array $arguments, Builder $component): void {
$items = array_move_after($component->getState(), $arguments['item']);
$component->state($items);
$component->callAfterStateUpdated();
})
->iconButton()
->size(ActionSize::Small)
->visible(fn (Builder $component): bool => $component->isReorderable());
if ($this->modifyMoveDownActionUsing) {
$action = $this->evaluate($this->modifyMoveDownActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function moveDownAction(?Closure $callback): static
{
$this->modifyMoveDownActionUsing = $callback;
return $this;
}
public function getMoveDownActionName(): string
{
return 'moveDown';
}
public function getMoveUpAction(): Action
{
$action = Action::make($this->getMoveUpActionName())
->label(__('filament-forms::components.builder.actions.move_up.label'))
->icon(FilamentIcon::resolve('forms::components.builder.actions.move-up') ?? 'heroicon-m-arrow-up')
->color('gray')
->action(function (array $arguments, Builder $component): void {
$items = array_move_before($component->getState(), $arguments['item']);
$component->state($items);
$component->callAfterStateUpdated();
})
->iconButton()
->size(ActionSize::Small)
->visible(fn (Builder $component): bool => $component->isReorderable());
if ($this->modifyMoveUpActionUsing) {
$action = $this->evaluate($this->modifyMoveUpActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function moveUpAction(?Closure $callback): static
{
$this->modifyMoveUpActionUsing = $callback;
return $this;
}
public function labelBetweenItems(string | Closure | null $label): static
{
$this->labelBetweenItems = $label;
return $this;
}
public function getMoveUpActionName(): string
{
return 'moveUp';
}
public function getReorderAction(): Action
{
$action = Action::make($this->getReorderActionName())
->label(__('filament-forms::components.builder.actions.reorder.label'))
->icon(FilamentIcon::resolve('forms::components.builder.actions.reorder') ?? 'heroicon-m-arrows-up-down')
->color('gray')
->action(function (array $arguments, Builder $component): void {
$items = [
...array_flip($arguments['items']),
...$component->getState(),
];
$component->state($items);
$component->callAfterStateUpdated();
})
->livewireClickHandlerEnabled(false)
->iconButton()
->size(ActionSize::Small)
->visible(fn (Builder $component): bool => $component->isReorderableWithDragAndDrop());
if ($this->modifyReorderActionUsing) {
$action = $this->evaluate($this->modifyReorderActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function reorderAction(?Closure $callback): static
{
$this->modifyReorderActionUsing = $callback;
return $this;
}
public function getReorderActionName(): string
{
return 'reorder';
}
public function getCollapseAction(): Action
{
$action = Action::make($this->getCollapseActionName())
->label(__('filament-forms::components.builder.actions.collapse.label'))
->icon(FilamentIcon::resolve('forms::components.builder.actions.collapse') ?? 'heroicon-m-chevron-up')
->color('gray')
->livewireClickHandlerEnabled(false)
->iconButton()
->size(ActionSize::Small);
if ($this->modifyCollapseActionUsing) {
$action = $this->evaluate($this->modifyCollapseActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function collapseAction(?Closure $callback): static
{
$this->modifyCollapseActionUsing = $callback;
return $this;
}
public function getCollapseActionName(): string
{
return 'collapse';
}
public function getExpandAction(): Action
{
$action = Action::make($this->getExpandActionName())
->label(__('filament-forms::components.builder.actions.expand.label'))
->icon(FilamentIcon::resolve('forms::components.builder.actions.expand') ?? 'heroicon-m-chevron-down')
->color('gray')
->livewireClickHandlerEnabled(false)
->iconButton()
->size(ActionSize::Small);
if ($this->modifyExpandActionUsing) {
$action = $this->evaluate($this->modifyExpandActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function expandAction(?Closure $callback): static
{
$this->modifyExpandActionUsing = $callback;
return $this;
}
public function getExpandActionName(): string
{
return 'expand';
}
public function getCollapseAllAction(): Action
{
$action = Action::make($this->getCollapseAllActionName())
->label(__('filament-forms::components.builder.actions.collapse_all.label'))
->color('gray')
->livewireClickHandlerEnabled(false)
->link()
->size(ActionSize::Small);
if ($this->modifyCollapseAllActionUsing) {
$action = $this->evaluate($this->modifyCollapseAllActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function collapseAllAction(?Closure $callback): static
{
$this->modifyCollapseAllActionUsing = $callback;
return $this;
}
public function getCollapseAllActionName(): string
{
return 'collapseAll';
}
public function getExpandAllAction(): Action
{
$action = Action::make($this->getExpandAllActionName())
->label(__('filament-forms::components.builder.actions.expand_all.label'))
->color('gray')
->livewireClickHandlerEnabled(false)
->link()
->size(ActionSize::Small);
if ($this->modifyExpandAllActionUsing) {
$action = $this->evaluate($this->modifyExpandAllActionUsing, [
'action' => $action,
]) ?? $action;
}
return $action;
}
public function expandAllAction(?Closure $callback): static
{
$this->modifyExpandAllActionUsing = $callback;
return $this;
}
public function getExpandAllActionName(): string
{
return 'expandAll';
}
public function truncateBlockLabel(bool | Closure $condition = true): static
{
$this->isBlockLabelTruncated = $condition;
return $this;
}
public function addBetweenActionLabel(string | Closure | null $label): static
{
$this->addBetweenActionLabel = $label;
return $this;
}
/**
* @deprecated Use `addBetweenActionLabel()` instead.
*/
public function createItemBetweenButtonLabel(string | Closure | null $label): static
{
$this->addBetweenActionLabel($label);
return $this;
}
public function addActionLabel(string | Closure | null $label): static
{
$this->addActionLabel = $label;
return $this;
}
/**
* @deprecated Use `addActionLabel()` instead.
*/
public function createItemButtonLabel(string | Closure | null $label): static
{
$this->addActionLabel($label);
return $this;
}
public function addable(bool | Closure $condition = true): static
{
$this->isAddable = $condition;
return $this;
}
public function deletable(bool | Closure $condition = true): static
{
$this->isDeletable = $condition;
return $this;
}
public function reorderable(bool | Closure $condition = true): static
{
$this->isReorderable = $condition;
return $this;
}
/**
* @deprecated Use `addable()` instead.
*/
public function disableItemCreation(bool | Closure $condition = true): static
{
$this->addable(fn (Builder $component): bool => ! $this->evaluate($condition));
return $this;
}
/**
* @deprecated Use `deletable()` instead.
*/
public function disableItemDeletion(bool | Closure $condition = true): static
{
$this->deletable(fn (Builder $component): bool => ! $this->evaluate($condition));
return $this;
}
/**
* @deprecated Use `reorderable()` instead.
*/
public function disableItemMovement(bool | Closure $condition = true): static
{
$this->reorderable(fn (Builder $component): bool => ! $this->evaluate($condition));
return $this;
}
/**
* @deprecated No longer part of the design system.
*/
public function inset(bool | Closure $condition = true): static
{
return $this;
}
public function reorderableWithDragAndDrop(bool | Closure $condition = true): static
{
$this->isReorderableWithDragAndDrop = $condition;
return $this;
}
public function reorderableWithButtons(bool | Closure $condition = true): static
{
$this->isReorderableWithButtons = $condition;
return $this;
}
/**
* @deprecated Use `blockLabels()` instead.
*/
public function showBlockLabels(bool | Closure $condition = true): static
{
$this->withBlockLabels($condition);
return $this;
}
/**
* @deprecated Use `blockLabels()` instead.
*/
public function withBlockLabels(bool | Closure $condition = true): static
{
$this->blockLabels($condition);
return $this;
}
/**
* @deprecated Use `blockNumbers()` instead.
*/
public function withBlockNumbers(bool | Closure $condition = true): static
{
$this->blockNumbers($condition);
return $this;
}
public function blockLabels(bool | Closure $condition = true): static
{
$this->hasBlockLabels = $condition;
return $this;
}
public function blockNumbers(bool | Closure $condition = true): static
{
$this->hasBlockNumbers = $condition;
return $this;
}
public function getBlock(string $name): ?Block
{
return Arr::first(
$this->getBlocks(),
fn (Block $block): bool => $block->getName() === $name,
);
}
/**
* @return array<Component>
*/
public function getBlocks(): array
{
return $this->getChildComponentContainer()->getComponents();
}
public function getChildComponentContainers(bool $withHidden = false): array
{
if ((! $withHidden) && $this->isHidden()) {
return [];
}
return collect($this->getState())
->filter(fn (array $itemData): bool => $this->hasBlock($itemData['type']))
->map(
fn (array $itemData, $itemIndex): ComponentContainer => $this
->getBlock($itemData['type'])
->getChildComponentContainer()
->statePath("{$itemIndex}.data")
->inlineLabel(false)
->getClone(),
)
->all();
}
public function getAddBetweenActionLabel(): string
{
return $this->evaluate($this->addBetweenActionLabel) ?? __('filament-forms::components.builder.actions.add_between.label');
}
public function getAddActionLabel(): string
{
return $this->evaluate($this->addActionLabel) ?? __('filament-forms::components.builder.actions.add.label', [
'label' => Str::lcfirst($this->getLabel()),
]);
}
public function hasBlock(string $name): bool
{
return (bool) $this->getBlock($name);
}
public function isReorderable(): bool
{
if ($this->isDisabled()) {
return false;
}
return (bool) $this->evaluate($this->isReorderable);
}
public function isReorderableWithDragAndDrop(): bool
{
return $this->evaluate($this->isReorderableWithDragAndDrop) && $this->isReorderable();
}
public function isReorderableWithButtons(): bool
{
return $this->evaluate($this->isReorderableWithButtons) && $this->isReorderable();
}
public function isAddable(): bool
{
if ($this->isDisabled()) {
return false;
}
if (filled($this->getMaxItems()) && ($this->getMaxItems() <= $this->getItemsCount())) {
return false;
}
return (bool) $this->evaluate($this->isAddable);
}
public function isDeletable(): bool
{
if ($this->isDisabled()) {
return false;
}
return (bool) $this->evaluate($this->isDeletable);
}
public function hasBlockLabels(): bool
{
return (bool) $this->evaluate($this->hasBlockLabels);
}
public function hasBlockNumbers(): bool
{
return (bool) $this->evaluate($this->hasBlockNumbers);
}
public function canConcealComponents(): bool
{
return $this->isCollapsible();
}
public function getLabelBetweenItems(): ?string
{
return $this->evaluate($this->labelBetweenItems);
}
public function isBlockLabelTruncated(): bool
{
return (bool) $this->evaluate($this->isBlockLabelTruncated);
}
/**
* @param array<string, int | string | null> | int | string | null $columns
*/
public function blockPickerColumns(array | int | string | null $columns = 2): static
{
if (! is_array($columns)) {
$columns = [
'lg' => $columns,
];
}
$this->blockPickerColumns = [
...($this->blockPickerColumns ?? []),
...$columns,
];
return $this;
}
/**
* @return array<string, int | string | null> | int | string | null
*/
public function getBlockPickerColumns(?string $breakpoint = null): array | int | string | null
{
$columns = $this->blockPickerColumns ?? [
'default' => 1,
'sm' => null,
'md' => null,
'lg' => null,
'xl' => null,
'2xl' => null,
];
if ($breakpoint !== null) {
return $columns[$breakpoint] ?? null;
}
return $columns;
}
public function blockPickerWidth(MaxWidth | string | Closure | null $width): static
{
$this->blockPickerWidth = $width;
return $this;
}
public function getBlockPickerWidth(): MaxWidth | string | null
{
$width = $this->evaluate($this->blockPickerWidth);
if (filled($width)) {
return $width;
}
$columns = $this->getBlockPickerColumns();
if (empty($columns)) {
return null;
}
return match (max($columns)) {
2 => 'md',
3 => '2xl',
4 => '4xl',
5 => '6xl',
6 => '7xl',
default => null,
};
}
/**
* @return array<string, mixed>
*/
public function getItemState(string $uuid): array
{
return $this->getChildComponentContainer($uuid)->getState(shouldCallHooksBefore: false);
}
/**
* @return array<string, mixed>
*/
public function getRawItemState(string $uuid): array
{
return $this->getChildComponentContainer($uuid)->getRawState();
}
}