mirror of
https://github.com/filamentphp/filament.git
synced 2026-08-30 17:07:25 +08:00
1716 lines
60 KiB
PHP
1716 lines
60 KiB
PHP
<?php
|
|
|
|
namespace Filament\Forms\Components;
|
|
|
|
use Closure;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\Builder\Block;
|
|
use Filament\Forms\View\FormsIconAlias;
|
|
use Filament\Schemas\Components\Concerns\CanBeCollapsed;
|
|
use Filament\Schemas\Components\Contracts\CanConcealComponents;
|
|
use Filament\Schemas\Components\Contracts\HasExtraItemActions;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Support\Components\Contracts\HasEmbeddedView;
|
|
use Filament\Support\Components\ViewComponent;
|
|
use Filament\Support\Concerns\HasReorderAnimationDuration;
|
|
use Filament\Support\Enums\Alignment;
|
|
use Filament\Support\Enums\GridDirection;
|
|
use Filament\Support\Enums\Size;
|
|
use Filament\Support\Enums\Width;
|
|
use Filament\Support\Facades\FilamentIcon;
|
|
use Filament\Support\Icons\Heroicon;
|
|
use Filament\Support\View\ComponentAttributeBag as FilamentComponentAttributeBag;
|
|
use Filament\Support\View\Components\DropdownComponent\ItemComponent;
|
|
use Filament\Support\View\Components\DropdownComponent\ItemComponent\IconComponent;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\HtmlString;
|
|
use Illuminate\Support\Js;
|
|
use Illuminate\Support\Str;
|
|
|
|
use function Filament\Forms\array_move_after;
|
|
use function Filament\Forms\array_move_before;
|
|
use function Filament\Support\generate_icon_html;
|
|
use function Filament\Support\generate_loading_indicator_html;
|
|
|
|
class Builder extends Field implements CanConcealComponents, HasEmbeddedView, HasExtraItemActions
|
|
{
|
|
use CanBeCollapsed;
|
|
use Concerns\CanBeCloned;
|
|
use Concerns\CanGenerateUuids;
|
|
use Concerns\CanLimitItemsLength;
|
|
use Concerns\HasExtraItemActions;
|
|
use HasReorderAnimationDuration;
|
|
|
|
protected ?string $publishedViewOverrideCheckPath = '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 bool | Closure $hasBlockIcons = false;
|
|
|
|
protected bool | Closure $hasBlockHeaders = true;
|
|
|
|
protected bool | Closure $hasBlockPreviews = false;
|
|
|
|
protected bool | Closure $hasInteractiveBlockPreviews = false;
|
|
|
|
protected Alignment | string | Closure | null $addActionAlignment = null;
|
|
|
|
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 ?Closure $modifyEditActionUsing = null;
|
|
|
|
protected string | Closure | null $labelBetweenItems = null;
|
|
|
|
protected bool | Closure $isBlockLabelTruncated = true;
|
|
|
|
/**
|
|
* @var array<string | int, int | Closure | null> | null
|
|
*/
|
|
protected ?array $blockPickerColumns = [];
|
|
|
|
protected Width | string | Closure | null $blockPickerWidth = null;
|
|
|
|
protected bool | Closure | null $shouldPartiallyRenderAfterActionsCalled = null;
|
|
|
|
/**
|
|
* @var array<?string> | null
|
|
*/
|
|
protected ?array $cachedItemsRawStateStructure = null;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->default([]);
|
|
|
|
$this->afterStateHydrated(static function (Builder $component): void {
|
|
$component->hydrateItems();
|
|
});
|
|
|
|
$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->getEditAction(),
|
|
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 ?? []);
|
|
});
|
|
}
|
|
|
|
public function hydrateItems(): void
|
|
{
|
|
$items = [];
|
|
|
|
foreach ($this->getRawState() ?? [] as $itemData) {
|
|
if ($uuid = $this->generateUuid()) {
|
|
$items[$uuid] = $itemData;
|
|
} else {
|
|
$items[] = $itemData;
|
|
}
|
|
}
|
|
|
|
$this->rawState($items);
|
|
}
|
|
|
|
/**
|
|
* @param array<Block> | Closure $blocks
|
|
*/
|
|
public function blocks(array | Closure $blocks): static
|
|
{
|
|
$this->components($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, array $data = []): void {
|
|
$newUuid = $component->generateUuid();
|
|
|
|
$items = $component->getRawState();
|
|
|
|
if ($newUuid) {
|
|
$items[$newUuid] = [
|
|
'type' => $arguments['block'],
|
|
'data' => $data,
|
|
];
|
|
} else {
|
|
$items[] = [
|
|
'type' => $arguments['block'],
|
|
'data' => $data,
|
|
];
|
|
}
|
|
|
|
$component->rawState($items);
|
|
|
|
$component->getChildSchema($newUuid ?? array_key_last($items))->fill(filled($data) ? $data : null);
|
|
|
|
$component->collapsed(false, shouldMakeComponentCollapsible: false);
|
|
|
|
$component->callAfterStateUpdated();
|
|
})
|
|
->livewireClickHandlerEnabled(false)
|
|
->button()
|
|
->size(Size::Small)
|
|
->visible(fn (Builder $component): bool => $component->isAddable());
|
|
|
|
if ($this->hasBlockPreviews()) {
|
|
$action
|
|
->modalHeading(fn (Builder $component) => __('filament-forms::components.builder.actions.add.modal.heading', [
|
|
'label' => $component->getLabel(),
|
|
]))
|
|
->modalSubmitActionLabel(__('filament-forms::components.builder.actions.add.modal.actions.add.label'))
|
|
->schema(function (array $arguments, Builder $component): array {
|
|
return $component->getBlock($arguments['block'])->getClone()->getDefaultChildComponents();
|
|
});
|
|
}
|
|
|
|
if ($this->modifyAddActionUsing) {
|
|
$action = $this->evaluate($this->modifyAddActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function addActionAlignment(Alignment | string | Closure | null $addActionAlignment): static
|
|
{
|
|
$this->addActionAlignment = $addActionAlignment;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getAddActionAlignment(): Alignment | string | null
|
|
{
|
|
$alignment = $this->evaluate($this->addActionAlignment);
|
|
|
|
if (is_string($alignment)) {
|
|
$alignment = Alignment::tryFrom($alignment) ?? $alignment;
|
|
}
|
|
|
|
return $alignment;
|
|
}
|
|
|
|
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, array $data = []): void {
|
|
$newKey = $component->generateUuid();
|
|
|
|
$items = [];
|
|
|
|
foreach ($component->getRawState() ?? [] as $key => $item) {
|
|
$items[$key] = $item;
|
|
|
|
if ($key === $arguments['afterItem']) {
|
|
if ($newKey) {
|
|
$items[$newKey] = [
|
|
'type' => $arguments['block'],
|
|
'data' => $data,
|
|
];
|
|
} else {
|
|
$items[] = [
|
|
'type' => $arguments['block'],
|
|
'data' => $data,
|
|
];
|
|
|
|
$newKey = array_key_last($items);
|
|
}
|
|
}
|
|
}
|
|
|
|
$component->rawState($items);
|
|
|
|
$component->getChildSchema($newKey)->fill(filled($data) ? $data : null);
|
|
|
|
$component->collapsed(false, shouldMakeComponentCollapsible: false);
|
|
|
|
$component->callAfterStateUpdated();
|
|
|
|
$component->shouldPartiallyRenderAfterActionsCalled() ? $component->partiallyRender() : null;
|
|
})
|
|
->livewireClickHandlerEnabled(false)
|
|
->button()
|
|
->size(Size::Small)
|
|
->visible(fn (Builder $component): bool => $component->isAddable());
|
|
|
|
if ($this->hasBlockPreviews()) {
|
|
$action
|
|
->modalHeading(fn (Builder $component) => __('filament-forms::components.builder.actions.add_between.modal.heading', [
|
|
'label' => $component->getLabel(),
|
|
]))
|
|
->modalSubmitActionLabel(__('filament-forms::components.builder.actions.add_between.modal.actions.add.label'))
|
|
->schema(function (array $arguments, Builder $component): array {
|
|
return $component->getBlock($arguments['block'])->getClone()->getDefaultChildComponents();
|
|
});
|
|
}
|
|
|
|
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(FormsIconAlias::COMPONENTS_BUILDER_ACTIONS_CLONE) ?? Heroicon::Square2Stack)
|
|
->color('gray')
|
|
->action(function (array $arguments, Builder $component): void {
|
|
$newUuid = $component->generateUuid();
|
|
|
|
$items = $component->getRawState();
|
|
|
|
if ($newUuid) {
|
|
$items[$newUuid] = $items[$arguments['item']];
|
|
} else {
|
|
$items[] = $items[$arguments['item']];
|
|
}
|
|
|
|
$component->rawState($items);
|
|
|
|
$component->collapsed(false, shouldMakeComponentCollapsible: false);
|
|
|
|
$component->callAfterStateUpdated();
|
|
|
|
$component->shouldPartiallyRenderAfterActionsCalled() ? $component->partiallyRender() : null;
|
|
})
|
|
->iconButton()
|
|
->size(Size::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(FormsIconAlias::COMPONENTS_BUILDER_ACTIONS_DELETE) ?? Heroicon::Trash)
|
|
->color('danger')
|
|
->action(function (array $arguments, Builder $component): void {
|
|
$items = $component->getRawState();
|
|
unset($items[$arguments['item']]);
|
|
|
|
$component->rawState($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
|
|
$component->shouldPartiallyRenderAfterActionsCalled() ? $component->partiallyRender() : null;
|
|
})
|
|
->iconButton()
|
|
->size(Size::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(FormsIconAlias::COMPONENTS_BUILDER_ACTIONS_MOVE_DOWN) ?? Heroicon::ArrowDown)
|
|
->color('gray')
|
|
->action(function (array $arguments, Builder $component): void {
|
|
$items = array_move_after($component->getRawState(), $arguments['item']);
|
|
|
|
$component->rawState($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
|
|
$component->shouldPartiallyRenderAfterActionsCalled() ? $component->partiallyRender() : null;
|
|
})
|
|
->iconButton()
|
|
->size(Size::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(FormsIconAlias::COMPONENTS_BUILDER_ACTIONS_MOVE_UP) ?? Heroicon::ArrowUp)
|
|
->color('gray')
|
|
->action(function (array $arguments, Builder $component): void {
|
|
$items = array_move_before($component->getRawState(), $arguments['item']);
|
|
|
|
$component->rawState($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
|
|
$component->shouldPartiallyRenderAfterActionsCalled() ? $component->partiallyRender() : null;
|
|
})
|
|
->iconButton()
|
|
->size(Size::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(FormsIconAlias::COMPONENTS_BUILDER_ACTIONS_REORDER) ?? Heroicon::ArrowsUpDown)
|
|
->color('gray')
|
|
->action(function (array $arguments, Builder $component): void {
|
|
$items = [
|
|
...array_flip($arguments['items']),
|
|
...$component->getRawState(),
|
|
];
|
|
|
|
$component->rawState($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
|
|
$component->shouldPartiallyRenderAfterActionsCalled() ? $component->partiallyRender() : null;
|
|
})
|
|
->livewireClickHandlerEnabled(false)
|
|
->iconButton()
|
|
->size(Size::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(FormsIconAlias::COMPONENTS_BUILDER_ACTIONS_COLLAPSE) ?? Heroicon::ChevronUp)
|
|
->color('gray')
|
|
->livewireClickHandlerEnabled(false)
|
|
->iconButton()
|
|
->size(Size::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(FormsIconAlias::COMPONENTS_BUILDER_ACTIONS_EXPAND) ?? Heroicon::ChevronDown)
|
|
->color('gray')
|
|
->livewireClickHandlerEnabled(false)
|
|
->iconButton()
|
|
->size(Size::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(Size::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(Size::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 getEditAction(): Action
|
|
{
|
|
$action = Action::make($this->getEditActionName())
|
|
->label(__('filament-forms::components.builder.actions.edit.label'))
|
|
->modalHeading(__('filament-forms::components.builder.actions.edit.modal.heading'))
|
|
->modalSubmitActionLabel(__('filament-forms::components.builder.actions.edit.modal.actions.save.label'))
|
|
->color('gray')
|
|
->fillForm(function (array $arguments, Builder $component) {
|
|
$state = $component->getState();
|
|
|
|
return $state[$arguments['item']]['data'];
|
|
})
|
|
->schema(function (array $arguments, Builder $component) {
|
|
return $component->getChildSchema($arguments['item'])
|
|
->getClone()
|
|
->getComponents(withHidden: true);
|
|
})
|
|
->action(function (array $arguments, Builder $component, $data): void {
|
|
$state = $component->getRawState();
|
|
|
|
$state[$arguments['item']]['data'] = $data;
|
|
|
|
$component->rawState($state);
|
|
|
|
$component->getChildSchema($arguments['item'])->fill($data);
|
|
|
|
$component->callAfterStateUpdated();
|
|
|
|
$component->shouldPartiallyRenderAfterActionsCalled() ? $component->partiallyRender() : null;
|
|
})
|
|
->iconButton()
|
|
->icon(Heroicon::Cog6Tooth)
|
|
->size(Size::Small)
|
|
->visible(fn (Builder $component): bool => (! $component->isDisabled()) && $component->hasBlockPreviews());
|
|
|
|
if ($this->modifyEditActionUsing) {
|
|
$action = $this->evaluate($this->modifyEditActionUsing, [
|
|
'action' => $action,
|
|
]) ?? $action;
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
|
|
public function editAction(?Closure $callback): static
|
|
{
|
|
$this->modifyEditActionUsing = $callback;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getEditActionName(): string
|
|
{
|
|
return 'edit';
|
|
}
|
|
|
|
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 blockIcons(bool | Closure $condition = true): static
|
|
{
|
|
$this->hasBlockIcons = $condition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function blockPreviews(bool | Closure $condition = true, bool | Closure $areInteractive = false): static
|
|
{
|
|
$this->hasBlockPreviews = $condition;
|
|
$this->hasInteractiveBlockPreviews = $areInteractive;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getBlock(string $name): ?Block
|
|
{
|
|
return Arr::first(
|
|
$this->getBlocks(),
|
|
fn (Block $block): bool => $block->getName() === $name,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<Block>
|
|
*/
|
|
public function getBlocks(): array
|
|
{
|
|
/** @var array<Block> $blocks */
|
|
$blocks = $this->getChildSchema()->getComponents();
|
|
|
|
return $blocks;
|
|
}
|
|
|
|
/**
|
|
* @return array<Schema>
|
|
*/
|
|
public function getItems(): array
|
|
{
|
|
return $this->getCachedDefaultChildSchemas();
|
|
}
|
|
|
|
/**
|
|
* @return array<Schema>
|
|
*/
|
|
public function getDefaultChildSchemas(): array
|
|
{
|
|
$rawState = $this->getRawState();
|
|
|
|
$this->cachedItemsRawStateStructure = $this->getRawStateStructure($rawState);
|
|
|
|
$blocks = [];
|
|
|
|
foreach ($this->getBlocks() as $block) {
|
|
$blocks[$block->getName()] = $block;
|
|
}
|
|
|
|
return collect($rawState)
|
|
->filter(fn (array $itemData): bool => filled($itemData['type'] ?? null) && array_key_exists($itemData['type'], $blocks))
|
|
->map(
|
|
fn (array $itemData, $itemIndex): Schema => $blocks[$itemData['type']]
|
|
->getChildSchema()
|
|
->statePath("{$itemIndex}.data")
|
|
->constantState($itemData['data'] ?? [])
|
|
->inlineLabel(false)
|
|
->getClone(),
|
|
)
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* Item schemas only depend on the raw state's structure - the item keys, their
|
|
* order, and each item's block type - since fields inside the items read their
|
|
* values from the live raw state. Comparing a structural fingerprint instead of
|
|
* the item values keeps this check cheap when it runs often, and avoids
|
|
* rebuilding the item schemas every time a value inside an item changes.
|
|
*/
|
|
protected function areCachedDefaultChildSchemasFresh(): bool
|
|
{
|
|
return $this->cachedItemsRawStateStructure === $this->getRawStateStructure($this->getRawState());
|
|
}
|
|
|
|
/**
|
|
* @return array<?string>
|
|
*/
|
|
protected function getRawStateStructure(mixed $rawState): array
|
|
{
|
|
return array_map(
|
|
static fn (mixed $itemData): ?string => is_array($itemData) ? ($itemData['type'] ?? null) : null,
|
|
is_array($rawState) ? $rawState : [],
|
|
);
|
|
}
|
|
|
|
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 hasBlockIcons(): bool
|
|
{
|
|
return (bool) $this->evaluate($this->hasBlockIcons);
|
|
}
|
|
|
|
public function hasBlockPreviews(): bool
|
|
{
|
|
return (bool) $this->evaluate($this->hasBlockPreviews);
|
|
}
|
|
|
|
public function hasInteractiveBlockPreviews(): bool
|
|
{
|
|
return (bool) $this->evaluate($this->hasInteractiveBlockPreviews);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @return array<Block>
|
|
*/
|
|
public function getBlockPickerBlocks(): array
|
|
{
|
|
$state = $this->getRawState();
|
|
|
|
/** @var array<Block> $blocks */
|
|
$blocks = array_filter($this->getBlocks(), function (Block $block) use ($state): bool {
|
|
/** @var Block $block */
|
|
$maxItems = $block->getMaxItems();
|
|
|
|
if ($maxItems === null) {
|
|
return true;
|
|
}
|
|
|
|
$count = count(array_filter($state, function (array $item) use ($block): bool {
|
|
return $item['type'] === $block->getName();
|
|
}));
|
|
|
|
return $count < $maxItems;
|
|
});
|
|
|
|
return $blocks;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, int | Closure | null> | int | Closure | null $columns
|
|
*/
|
|
public function blockPickerColumns(array | int | Closure | null $columns = 2): static
|
|
{
|
|
if ($columns instanceof Closure) {
|
|
$this->blockPickerColumns[] = $columns;
|
|
|
|
return $this;
|
|
}
|
|
|
|
if (! is_array($columns)) {
|
|
$columns = [
|
|
'lg' => $columns,
|
|
];
|
|
}
|
|
|
|
$this->blockPickerColumns = [
|
|
...($this->blockPickerColumns ?? []),
|
|
...$columns,
|
|
];
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, ?int> | int | null
|
|
*/
|
|
public function getBlockPickerColumns(?string $breakpoint = null): array | int | null
|
|
{
|
|
$columns = $this->blockPickerColumns ?? [
|
|
'default' => 1,
|
|
'sm' => null,
|
|
'md' => null,
|
|
'lg' => null,
|
|
'xl' => null,
|
|
'2xl' => null,
|
|
];
|
|
|
|
foreach ($this->blockPickerColumns ?? [] as $columnBreakpoint => $column) {
|
|
$column = $this->evaluate($column);
|
|
|
|
if (is_array($column)) {
|
|
$columns = [
|
|
...$columns,
|
|
...$column,
|
|
];
|
|
|
|
unset($columns[$columnBreakpoint]);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (blank($columnBreakpoint)) {
|
|
unset($columns[$columnBreakpoint]);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! is_string($columnBreakpoint)) {
|
|
$columns['lg'] = $column;
|
|
|
|
unset($columns[$columnBreakpoint]);
|
|
|
|
continue;
|
|
}
|
|
|
|
$columns[$columnBreakpoint] = $column;
|
|
}
|
|
|
|
if ($breakpoint !== null) {
|
|
return $columns[$breakpoint] ?? null;
|
|
}
|
|
|
|
return $columns;
|
|
}
|
|
|
|
public function blockPickerWidth(Width | string | Closure | null $width): static
|
|
{
|
|
$this->blockPickerWidth = $width;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getBlockPickerWidth(): Width | 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 $key): array
|
|
{
|
|
return $this->getChildSchema($key)->getState(shouldCallHooksBefore: false);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getRawItemState(string $key): array
|
|
{
|
|
return $this->getChildSchema($key)->getStateSnapshot();
|
|
}
|
|
|
|
public function getHeadingsCount(): int
|
|
{
|
|
if (! $this->hasBlockLabels()) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array<mixed>> $rules
|
|
*/
|
|
public function dehydrateValidationRules(array &$rules): void
|
|
{
|
|
parent::dehydrateValidationRules($rules);
|
|
|
|
$rules["{$this->getStatePath()}.*.type"] = ['required'];
|
|
}
|
|
|
|
public function partiallyRenderAfterActionsCalled(bool | Closure | null $condition = true): static
|
|
{
|
|
$this->shouldPartiallyRenderAfterActionsCalled = $condition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function shouldPartiallyRenderAfterActionsCalled(): bool
|
|
{
|
|
$condition = $this->evaluate($this->shouldPartiallyRenderAfterActionsCalled);
|
|
|
|
if ($condition !== null) {
|
|
return (bool) $condition;
|
|
}
|
|
|
|
return ! $this->isLive();
|
|
}
|
|
|
|
public function blockHeaders(bool | Closure $condition = true): static
|
|
{
|
|
$this->hasBlockHeaders = $condition;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function hasBlockHeaders(): bool
|
|
{
|
|
return (bool) $this->evaluate($this->hasBlockHeaders);
|
|
}
|
|
|
|
/**
|
|
* @param array<Block> $blocks
|
|
* @param array<string, ?int> | int | null $columns
|
|
*/
|
|
protected function generateBlockPickerHtml(
|
|
Action $action,
|
|
array $blocks,
|
|
string $key,
|
|
string $triggerHtml,
|
|
Alignment | string | null $actionAlignment = null,
|
|
?string $afterItem = null,
|
|
array | int | null $columns = null,
|
|
Width | string | null $width = null,
|
|
): string {
|
|
/** @var view-string $publishedView */
|
|
$publishedView = 'filament-forms::components.builder.block-picker';
|
|
|
|
if (ViewComponent::hasPublishedEmbeddedViewOverride($publishedView)) {
|
|
return view($publishedView, [
|
|
'action' => $action,
|
|
'actionAlignment' => $actionAlignment,
|
|
'afterItem' => $afterItem,
|
|
'blocks' => $blocks,
|
|
'columns' => $columns,
|
|
'key' => $key,
|
|
'trigger' => new HtmlString($triggerHtml),
|
|
'width' => $width,
|
|
])->render();
|
|
}
|
|
|
|
$placement = match ($actionAlignment) {
|
|
Alignment::Start, Alignment::Left => 'bottom-start',
|
|
Alignment::End, Alignment::Right => 'bottom-end',
|
|
default => null,
|
|
};
|
|
|
|
$alignmentClass = ($actionAlignment instanceof Alignment)
|
|
? ('fi-align-' . $actionAlignment->value)
|
|
: (is_string($actionAlignment) ? $actionAlignment : null);
|
|
|
|
if (is_string($width)) {
|
|
$width = Width::tryFrom($width) ?? $width;
|
|
}
|
|
|
|
$widthClass = ($width instanceof Width)
|
|
? "fi-width-{$width->value}"
|
|
: (is_string($width) ? $width : null);
|
|
|
|
$xFloatDirective = 'x-float' . ($placement ? ".placement.{$placement}" : '') . '.flip.shift.offset';
|
|
|
|
$dropdownAttributes = (new FilamentComponentAttributeBag)
|
|
->merge(['x-data' => 'filamentDropdown'], escape: false)
|
|
->class([
|
|
'fi-dropdown',
|
|
'fi-fo-builder-block-picker',
|
|
$alignmentClass,
|
|
]);
|
|
|
|
$panelAttributes = (new FilamentComponentAttributeBag)
|
|
->merge([
|
|
'x-cloak' => true,
|
|
'x-ref' => 'panel',
|
|
'x-transition:enter-start' => 'fi-opacity-0',
|
|
'x-transition:leave-end' => 'fi-opacity-0',
|
|
$xFloatDirective => '{ offset: 8 }',
|
|
], escape: false)
|
|
->class([
|
|
'fi-dropdown-panel',
|
|
$widthClass,
|
|
]);
|
|
|
|
$loadingDelay = config('filament.livewire_loading_delay', 'default');
|
|
|
|
ob_start(); ?>
|
|
|
|
<div <?= $dropdownAttributes->toHtml() ?>>
|
|
<div
|
|
x-on:keyup.enter="toggle($event)"
|
|
x-on:keyup.space="toggle($event)"
|
|
x-on:mousedown="if ($event.button === 0) toggle($event)"
|
|
class="fi-dropdown-trigger"
|
|
>
|
|
<?= $triggerHtml ?>
|
|
</div>
|
|
|
|
<?php if (filled($blocks)) { ?>
|
|
<div <?= $panelAttributes->toHtml() ?>>
|
|
<div class="fi-dropdown-list">
|
|
<div <?= (new FilamentComponentAttributeBag)->grid($columns, GridDirection::Column)->toHtml() ?>>
|
|
<?php foreach ($blocks as $block) {
|
|
$blockIcon = $block->getIcon();
|
|
|
|
$wireClickArguments = ['block' => $block->getName()];
|
|
|
|
if (filled($afterItem)) {
|
|
$wireClickArguments['afterItem'] = $afterItem;
|
|
}
|
|
|
|
$wireClick = "mountAction('{$action->getName()}', " . Js::from($wireClickArguments) . ", { schemaComponent: '{$key}' })";
|
|
|
|
$itemAttributes = (new FilamentComponentAttributeBag)
|
|
->merge([
|
|
'x-on:click' => 'close',
|
|
'wire:click' => $wireClick,
|
|
'type' => 'button',
|
|
'wire:loading.attr' => 'disabled',
|
|
'wire:target' => $wireClick,
|
|
], escape: false)
|
|
->class(['fi-dropdown-list-item'])
|
|
->color(ItemComponent::class, 'gray');
|
|
?>
|
|
|
|
<button <?= $itemAttributes->toHtml() ?>>
|
|
<?php if (filled($blockIcon)) { ?>
|
|
<?= generate_icon_html($blockIcon, attributes: (new FilamentComponentAttributeBag([
|
|
'wire:loading.remove.delay.' . $loadingDelay => true,
|
|
'wire:target' => $wireClick,
|
|
]))->color(IconComponent::class, 'gray'))?->toHtml() ?>
|
|
<?php } ?>
|
|
|
|
<?= generate_loading_indicator_html((new FilamentComponentAttributeBag([
|
|
'wire:loading.delay.' . $loadingDelay => '',
|
|
'wire:target' => $wireClick,
|
|
]))->color(IconComponent::class, 'gray'))->toHtml() ?>
|
|
|
|
<span class="fi-dropdown-list-item-label">
|
|
<?= e($block->getLabel()) ?>
|
|
</span>
|
|
</button>
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php } ?>
|
|
</div>
|
|
|
|
<?php return ob_get_clean();
|
|
}
|
|
|
|
public function toEmbeddedHtml(): string
|
|
{
|
|
$items = $this->getItems();
|
|
|
|
// Filter before counting so `$itemCount` agrees with the loop's
|
|
// `$isFirst` / `$isLast` calculations.
|
|
$items = array_filter(
|
|
$items,
|
|
static fn ($item): bool => $item->getParentComponent() instanceof Block,
|
|
);
|
|
|
|
$blockPickerBlocks = $this->getBlockPickerBlocks();
|
|
$blockPickerColumns = $this->getBlockPickerColumns();
|
|
$blockPickerWidth = $this->getBlockPickerWidth();
|
|
$hasBlockPreviews = $this->hasBlockPreviews();
|
|
$hasInteractiveBlockPreviews = $this->hasInteractiveBlockPreviews();
|
|
|
|
$addAction = $this->getAction($this->getAddActionName());
|
|
$addActionAlignment = $this->getAddActionAlignment();
|
|
$addBetweenAction = $this->getAction($this->getAddBetweenActionName());
|
|
$cloneAction = $this->getAction($this->getCloneActionName());
|
|
$collapseAllAction = $this->getAction($this->getCollapseAllActionName());
|
|
$editAction = $this->getAction($this->getEditActionName());
|
|
$expandAllAction = $this->getAction($this->getExpandAllActionName());
|
|
$deleteAction = $this->getAction($this->getDeleteActionName());
|
|
$moveDownAction = $this->getAction($this->getMoveDownActionName());
|
|
$moveUpAction = $this->getAction($this->getMoveUpActionName());
|
|
$reorderAction = $this->getAction($this->getReorderActionName());
|
|
$extraItemActions = $this->getExtraItemActions();
|
|
|
|
$isAddable = $this->isAddable();
|
|
$isCloneable = $this->isCloneable();
|
|
$isCollapsible = $this->isCollapsible();
|
|
$isDeletable = $this->isDeletable();
|
|
$isReorderableWithButtons = $this->isReorderableWithButtons();
|
|
$isReorderableWithDragAndDrop = $this->isReorderableWithDragAndDrop();
|
|
|
|
$collapseAllActionIsVisible = $isCollapsible && $collapseAllAction->isVisible();
|
|
$expandAllActionIsVisible = $isCollapsible && $expandAllAction->isVisible();
|
|
$persistCollapsed = $this->shouldPersistCollapsed();
|
|
|
|
$key = $this->getKey();
|
|
$statePath = $this->getStatePath();
|
|
|
|
$blockLabelHeadingTag = $this->getHeadingTag();
|
|
$isBlockLabelTruncated = $this->isBlockLabelTruncated();
|
|
$labelBetweenItems = $this->getLabelBetweenItems();
|
|
|
|
$id = $this->getId();
|
|
|
|
$outerAttributes = (new FilamentComponentAttributeBag)
|
|
->merge($this->getExtraAttributes(), escape: false)
|
|
->merge([
|
|
'aria-labelledby' => "{$id}-label",
|
|
'id' => $id,
|
|
'role' => 'group',
|
|
], escape: false)
|
|
->class([
|
|
'fi-fo-builder',
|
|
'fi-collapsible' => $isCollapsible,
|
|
]);
|
|
|
|
$itemCount = count($items);
|
|
$itemIndex = 0;
|
|
|
|
$hasBlockLabels = $this->hasBlockLabels();
|
|
$hasBlockIcons = $this->hasBlockIcons();
|
|
$hasBlockNumbers = $this->hasBlockNumbers();
|
|
$hasBlockHeaders = $this->hasBlockHeaders();
|
|
|
|
ob_start(); ?>
|
|
|
|
<div <?= $outerAttributes->toHtml() ?>>
|
|
<?php if ($collapseAllActionIsVisible || $expandAllActionIsVisible) { ?>
|
|
<div
|
|
<?= (new FilamentComponentAttributeBag)->class([
|
|
'fi-fo-builder-actions',
|
|
'fi-hidden' => $itemCount < 2,
|
|
])->toHtml() ?>
|
|
>
|
|
<?php if ($collapseAllActionIsVisible) { ?>
|
|
<span x-on:click="$dispatch('builder-collapse', '<?= e($statePath) ?>')">
|
|
<?= $collapseAllAction->toHtml() ?>
|
|
</span>
|
|
<?php } ?>
|
|
|
|
<?php if ($expandAllActionIsVisible) { ?>
|
|
<span x-on:click="$dispatch('builder-expand', '<?= e($statePath) ?>')">
|
|
<?= $expandAllAction->toHtml() ?>
|
|
</span>
|
|
<?php } ?>
|
|
</div>
|
|
<?php } ?>
|
|
|
|
<?php if ($itemCount) { ?>
|
|
<ul
|
|
x-sortable
|
|
data-sortable-animation-duration="<?= e($this->getReorderAnimationDuration()) ?>"
|
|
x-on:end.stop="$wire.mountAction('reorder', { items: $event.target.sortable.toArray() }, { schemaComponent: '<?= e($key) ?>' })"
|
|
class="fi-fo-builder-items"
|
|
>
|
|
<?php foreach ($items as $itemKey => $item) {
|
|
/** @var Block $block */
|
|
$block = $item->getParentComponent();
|
|
|
|
$itemIndex++;
|
|
$isFirst = $itemIndex === 1;
|
|
$isLast = $itemIndex === $itemCount;
|
|
|
|
$visibleExtraItemActions = array_filter(
|
|
$extraItemActions,
|
|
fn (Action $action): bool => $action(['item' => $itemKey])->isVisible(),
|
|
);
|
|
$itemCloneAction = $cloneAction(['item' => $itemKey]);
|
|
$cloneActionIsVisible = $isCloneable && $itemCloneAction->isVisible();
|
|
$itemDeleteAction = $deleteAction(['item' => $itemKey]);
|
|
$deleteActionIsVisible = $isDeletable && $itemDeleteAction->isVisible();
|
|
$itemEditAction = $editAction(['item' => $itemKey]);
|
|
$editActionIsVisible = $hasBlockPreviews && $itemEditAction->isVisible();
|
|
$itemMoveDownAction = $moveDownAction(['item' => $itemKey])->disabled($isLast);
|
|
$moveDownActionIsVisible = $isReorderableWithButtons && $itemMoveDownAction->isVisible();
|
|
$itemMoveUpAction = $moveUpAction(['item' => $itemKey])->disabled($isFirst);
|
|
$moveUpActionIsVisible = $isReorderableWithButtons && $itemMoveUpAction->isVisible();
|
|
$reorderActionIsVisible = $isReorderableWithDragAndDrop && $reorderAction->isVisible();
|
|
$hasItemHeader = $hasBlockHeaders && ($reorderActionIsVisible || $moveUpActionIsVisible || $moveDownActionIsVisible || $hasBlockIcons || $hasBlockLabels || $editActionIsVisible || $cloneActionIsVisible || $deleteActionIsVisible || $isCollapsible || $visibleExtraItemActions);
|
|
?>
|
|
|
|
<li
|
|
wire:ignore.self
|
|
wire:key="<?= e($item->getLivewireKey()) ?>.item"
|
|
x-data="{
|
|
isCollapsed: <?php if ($persistCollapsed) { ?>$persist(<?= Js::from($this->isCollapsed($item)) ?>).as(`builder-${<?= Js::from($key) ?>}-${<?= Js::from($itemKey) ?>}-isCollapsed`)<?php } else { ?><?= Js::from($this->isCollapsed($item)) ?><?php } ?>,
|
|
}"
|
|
x-on:builder-expand.window="$event.detail === '<?= e($statePath) ?>' && (isCollapsed = false)"
|
|
x-on:builder-collapse.window="$event.detail === '<?= e($statePath) ?>' && (isCollapsed = true)"
|
|
x-on:expand="isCollapsed = false"
|
|
x-sortable-item="<?= e($itemKey) ?>"
|
|
<?= $block->getExtraAttributeBag()
|
|
->class([
|
|
'fi-fo-builder-item',
|
|
'fi-fo-builder-item-has-header' => $hasItemHeader,
|
|
])->toHtml() ?>
|
|
x-bind:class="{ 'fi-collapsed': isCollapsed }"
|
|
>
|
|
<?php if ($hasItemHeader) { ?>
|
|
<div
|
|
<?php if ($isCollapsible) { ?>
|
|
x-on:click.stop="isCollapsed = !isCollapsed"
|
|
<?php } ?>
|
|
class="fi-fo-builder-item-header"
|
|
>
|
|
<?php if ($reorderActionIsVisible || $moveUpActionIsVisible || $moveDownActionIsVisible) { ?>
|
|
<ul class="fi-fo-builder-item-header-start-actions">
|
|
<?php if ($reorderActionIsVisible) { ?>
|
|
<li x-on:click.stop>
|
|
<?= $reorderAction->extraAttributes(['x-sortable-handle' => true], merge: true)->toHtml() ?>
|
|
</li>
|
|
<?php } ?>
|
|
|
|
<?php if ($moveUpActionIsVisible || $moveDownActionIsVisible) { ?>
|
|
<li x-on:click.stop><?= $itemMoveUpAction->toHtml() ?></li>
|
|
<li x-on:click.stop><?= $itemMoveDownAction->toHtml() ?></li>
|
|
<?php } ?>
|
|
</ul>
|
|
<?php } ?>
|
|
|
|
<?php
|
|
$blockIcon = $block->getIcon();
|
|
?>
|
|
|
|
<?php if ($hasBlockIcons && filled($blockIcon)) { ?>
|
|
<?= generate_icon_html($blockIcon, attributes: (new FilamentComponentAttributeBag)->class(['fi-fo-builder-item-header-icon']))?->toHtml() ?>
|
|
<?php } ?>
|
|
|
|
<?php if ($hasBlockLabels) { ?>
|
|
<<?= e($blockLabelHeadingTag) ?>
|
|
<?= (new FilamentComponentAttributeBag)->class([
|
|
'fi-fo-builder-item-header-label',
|
|
'fi-truncated' => $isBlockLabelTruncated,
|
|
])->toHtml() ?>
|
|
>
|
|
<?= e($block->getLabel($item->getRawState(), $itemKey, $itemIndex - 1)) ?>
|
|
<?php if ($hasBlockNumbers) { ?>
|
|
<?= e($itemIndex) ?>
|
|
<?php } ?>
|
|
</<?= e($blockLabelHeadingTag) ?>>
|
|
<?php } ?>
|
|
|
|
<?php if ($editActionIsVisible || $cloneActionIsVisible || $deleteActionIsVisible || $isCollapsible || $visibleExtraItemActions) { ?>
|
|
<ul class="fi-fo-builder-item-header-end-actions">
|
|
<?php foreach ($visibleExtraItemActions as $extraItemAction) { ?>
|
|
<li x-on:click.stop><?= $extraItemAction(['item' => $itemKey])->toHtml() ?></li>
|
|
<?php } ?>
|
|
|
|
<?php if ($editActionIsVisible) { ?>
|
|
<li x-on:click.stop><?= $itemEditAction->toHtml() ?></li>
|
|
<?php } ?>
|
|
|
|
<?php if ($cloneActionIsVisible) { ?>
|
|
<li x-on:click.stop><?= $itemCloneAction->toHtml() ?></li>
|
|
<?php } ?>
|
|
|
|
<?php if ($deleteActionIsVisible) { ?>
|
|
<li x-on:click.stop><?= $itemDeleteAction->toHtml() ?></li>
|
|
<?php } ?>
|
|
|
|
<?php if ($isCollapsible) { ?>
|
|
<li class="fi-fo-builder-item-header-collapsible-actions" x-on:click.stop="isCollapsed = !isCollapsed">
|
|
<div class="fi-fo-builder-item-header-collapse-action">
|
|
<?= $this->getAction('collapse')->toHtml() ?>
|
|
</div>
|
|
<div class="fi-fo-builder-item-header-expand-action">
|
|
<?= $this->getAction('expand')->toHtml() ?>
|
|
</div>
|
|
</li>
|
|
<?php } ?>
|
|
</ul>
|
|
<?php } ?>
|
|
</div>
|
|
<?php } ?>
|
|
|
|
<div
|
|
x-show="! isCollapsed"
|
|
<?= (new FilamentComponentAttributeBag)->class([
|
|
'fi-fo-builder-item-content',
|
|
'fi-fo-builder-item-content-has-preview' => $hasBlockPreviews && $block->hasPreview(),
|
|
])->toHtml() ?>
|
|
>
|
|
<?php if ($hasBlockPreviews && $block->hasPreview()) { ?>
|
|
<div
|
|
<?= (new FilamentComponentAttributeBag)->class([
|
|
'fi-fo-builder-item-preview',
|
|
'fi-interactive' => $hasInteractiveBlockPreviews,
|
|
])->toHtml() ?>
|
|
>
|
|
<?= $block->renderPreview($item->getRawState())->render() ?>
|
|
</div>
|
|
|
|
<?php if ($editActionIsVisible && (! $hasInteractiveBlockPreviews)) { ?>
|
|
<div
|
|
class="fi-fo-builder-item-preview-edit-overlay"
|
|
role="button"
|
|
x-on:click.stop="<?= e('$wire.mountAction(\'edit\', { item: \'' . $itemKey . '\' }, { schemaComponent: \'' . $key . '\' })') ?>"
|
|
></div>
|
|
<?php } ?>
|
|
<?php } else { ?>
|
|
<?= $item->toHtml() ?>
|
|
<?php } ?>
|
|
</div>
|
|
</li>
|
|
|
|
<?php if (! $isLast) { ?>
|
|
<?php if ($isAddable && $addBetweenAction(['afterItem' => $itemKey])->isVisible()) { ?>
|
|
<li class="fi-fo-builder-add-between-items-ctn">
|
|
<div class="fi-fo-builder-add-between-items">
|
|
<div class="fi-fo-builder-block-picker-ctn">
|
|
<?= $this->generateBlockPickerHtml(
|
|
action: $addBetweenAction,
|
|
blocks: $blockPickerBlocks,
|
|
key: $key,
|
|
triggerHtml: $addBetweenAction(['afterItem' => $itemKey])->toHtml(),
|
|
afterItem: $itemKey,
|
|
columns: $blockPickerColumns,
|
|
width: $blockPickerWidth,
|
|
) ?>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<?php } elseif (filled($labelBetweenItems)) { ?>
|
|
<li class="fi-fo-builder-label-between-items-ctn">
|
|
<div class="fi-fo-builder-label-between-items-divider-before"></div>
|
|
<span class="fi-fo-builder-label-between-items"><?= e($labelBetweenItems) ?></span>
|
|
<div class="fi-fo-builder-label-between-items-divider-after"></div>
|
|
</li>
|
|
<?php } ?>
|
|
<?php } ?>
|
|
<?php } ?>
|
|
</ul>
|
|
<?php } ?>
|
|
|
|
<?php if ($isAddable && $addAction->isVisible()) { ?>
|
|
<?= $this->generateBlockPickerHtml(
|
|
action: $addAction,
|
|
blocks: $blockPickerBlocks,
|
|
key: $key,
|
|
triggerHtml: $addAction->toHtml(),
|
|
actionAlignment: $addActionAlignment,
|
|
columns: $blockPickerColumns,
|
|
width: $blockPickerWidth,
|
|
) ?>
|
|
<?php } ?>
|
|
</div>
|
|
|
|
<?php return $this->wrapEmbeddedHtml(ob_get_clean(), labelTag: 'div');
|
|
}
|
|
}
|