| null */ protected ?array $blockPickerColumns = []; protected Width | string | Closure | null $blockPickerWidth = null; protected bool | Closure | null $shouldPartiallyRenderAfterActionsCalled = null; /** * @var array | 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 | 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 */ public function getBlocks(): array { /** @var array $blocks */ $blocks = $this->getChildSchema()->getComponents(); return $blocks; } /** * @return array */ public function getItems(): array { return $this->getCachedDefaultChildSchemas(); } /** * @return array */ 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 */ 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 */ public function getBlockPickerBlocks(): array { $state = $this->getRawState(); /** @var array $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 | 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 | 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 */ public function getItemState(string $key): array { return $this->getChildSchema($key)->getState(shouldCallHooksBefore: false); } /** * @return array */ public function getRawItemState(string $key): array { return $this->getChildSchema($key)->getStateSnapshot(); } public function getHeadingsCount(): int { if (! $this->hasBlockLabels()) { return 0; } return 1; } /** * @param array> $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 $blocks * @param array | 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(); ?>
toHtml() ?>>
toHtml() ?>>
grid($columns, GridDirection::Column)->toHtml() ?>> 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'); ?>
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(); ?>
toHtml() ?>>
class([ 'fi-fo-builder-actions', 'fi-hidden' => $itemCount < 2, ])->toHtml() ?> > toHtml() ?> toHtml() ?>
    $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); ?>
  • getExtraAttributeBag() ->class([ 'fi-fo-builder-item', 'fi-fo-builder-item-has-header' => $hasItemHeader, ])->toHtml() ?> x-bind:class="{ 'fi-collapsed': isCollapsed }" >
    x-on:click.stop="isCollapsed = !isCollapsed" class="fi-fo-builder-item-header" >
    • extraAttributes(['x-sortable-handle' => true], merge: true)->toHtml() ?>
    • toHtml() ?>
    • toHtml() ?>
    getIcon(); ?> class(['fi-fo-builder-item-header-icon']))?->toHtml() ?> < class([ 'fi-fo-builder-item-header-label', 'fi-truncated' => $isBlockLabelTruncated, ])->toHtml() ?> > getLabel($item->getRawState(), $itemKey, $itemIndex - 1)) ?> >
    • $itemKey])->toHtml() ?>
    • toHtml() ?>
    • toHtml() ?>
    • toHtml() ?>
    • getAction('collapse')->toHtml() ?>
      getAction('expand')->toHtml() ?>
    class([ 'fi-fo-builder-item-content', 'fi-fo-builder-item-content-has-preview' => $hasBlockPreviews && $block->hasPreview(), ])->toHtml() ?> > hasPreview()) { ?>
    class([ 'fi-fo-builder-item-preview', 'fi-interactive' => $hasInteractiveBlockPreviews, ])->toHtml() ?> > renderPreview($item->getRawState())->render() ?>
    toHtml() ?>
  • $itemKey])->isVisible()) { ?>
  • generateBlockPickerHtml( action: $addBetweenAction, blocks: $blockPickerBlocks, key: $key, triggerHtml: $addBetweenAction(['afterItem' => $itemKey])->toHtml(), afterItem: $itemKey, columns: $blockPickerColumns, width: $blockPickerWidth, ) ?>
isVisible()) { ?> generateBlockPickerHtml( action: $addAction, blocks: $blockPickerBlocks, key: $key, triggerHtml: $addAction->toHtml(), actionAlignment: $addActionAlignment, columns: $blockPickerColumns, width: $blockPickerWidth, ) ?>
wrapEmbeddedHtml(ob_get_clean(), labelTag: 'div'); } }