diff --git a/src/LiveComponent/src/Form/Type/LiveCollectionType.php b/src/LiveComponent/src/Form/Type/LiveCollectionType.php index 2fc3aea95f3..a7d214b51ad 100644 --- a/src/LiveComponent/src/Form/Type/LiveCollectionType.php +++ b/src/LiveComponent/src/Form/Type/LiveCollectionType.php @@ -35,6 +35,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $prototype = $builder->create('delete', $options['button_delete_type'], $options['button_delete_options']); $builder->setAttribute('button_delete_prototype', $prototype->getForm()); } + + if ($options['allow_sort']) { + $moveUpPrototype = $builder->create('move_up', $options['button_move_up_type'], $options['button_move_up_options']); + $builder->setAttribute('button_move_up_prototype', $moveUpPrototype->getForm()); + + $moveDownPrototype = $builder->create('move_down', $options['button_move_down_type'], $options['button_move_down_options']); + $builder->setAttribute('button_move_down_prototype', $moveDownPrototype->getForm()); + } } public function buildView(FormView $view, FormInterface $form, array $options): void @@ -92,6 +100,53 @@ public function finishView(FormView $view, FormInterface $form, array $options): array_splice($entryView->vars['button_delete']->vars['block_prefixes'], 1, 0, 'live_collection_button_delete'); } } + + // Add move up and move down buttons + if ($form->getConfig()->hasAttribute('button_move_up_prototype')) { + $prototype = $form->getConfig()->getAttribute('button_move_up_prototype'); + + $prototypes = []; + foreach ($form as $k => $entry) { + $prototypes[$k] = clone $prototype; + $prototypes[$k]->setParent($entry); + } + + foreach ($view as $k => $entryView) { + $entryView->vars['button_move_up'] = $prototypes[$k]->createView($entryView); + + $attr = $entryView->vars['button_move_up']->vars['attr']; + $attr['data-action'] ??= 'live#action'; + $attr['data-live-action-param'] ??= 'moveCollectionItemUp'; + $attr['data-live-name-param'] ??= $view->vars['full_name']; + $attr['data-live-index-param'] ??= $k; + $entryView->vars['button_move_up']->vars['attr'] = $attr; + + array_splice($entryView->vars['button_move_up']->vars['block_prefixes'], 1, 0, 'live_collection_button_move_up'); + } + } + + if ($form->getConfig()->hasAttribute('button_move_down_prototype')) { + $prototype = $form->getConfig()->getAttribute('button_move_down_prototype'); + + $prototypes = []; + foreach ($form as $k => $entry) { + $prototypes[$k] = clone $prototype; + $prototypes[$k]->setParent($entry); + } + + foreach ($view as $k => $entryView) { + $entryView->vars['button_move_down'] = $prototypes[$k]->createView($entryView); + + $attr = $entryView->vars['button_move_down']->vars['attr']; + $attr['data-action'] ??= 'live#action'; + $attr['data-live-action-param'] ??= 'moveCollectionItemDown'; + $attr['data-live-name-param'] ??= $view->vars['full_name']; + $attr['data-live-index-param'] ??= $k; + $entryView->vars['button_move_down']->vars['attr'] = $attr; + + array_splice($entryView->vars['button_move_down']->vars['block_prefixes'], 1, 0, 'live_collection_button_move_down'); + } + } } public function configureOptions(OptionsResolver $resolver): void @@ -105,8 +160,14 @@ public function configureOptions(OptionsResolver $resolver): void 'button_add_options' => [], 'button_delete_type' => ButtonType::class, 'button_delete_options' => [], + 'button_move_up_type' => ButtonType::class, + 'button_move_up_options' => [], + 'button_move_down_type' => ButtonType::class, + 'button_move_down_options' => [], 'allow_add' => true, 'allow_delete' => true, + 'allow_sort' => false, + 'order_property_path' => null, 'by_reference' => false, ]); } diff --git a/src/LiveComponent/src/LiveCollectionTrait.php b/src/LiveComponent/src/LiveCollectionTrait.php index 70f11f23b8b..ac2fa595a40 100644 --- a/src/LiveComponent/src/LiveCollectionTrait.php +++ b/src/LiveComponent/src/LiveCollectionTrait.php @@ -46,6 +46,70 @@ public function removeCollectionItem(PropertyAccessorInterface $propertyAccessor $propertyAccessor->setValue($this->formValues, $propertyPath, $data); } + #[LiveAction] + public function moveCollectionItemUp(PropertyAccessorInterface $propertyAccessor, #[LiveArg] string $name, #[LiveArg] int $index): void + { + if ($index <= 0) { + return; + } + + $propertyPath = $this->fieldNameToPropertyPath($name, $this->formName); + $data = $propertyAccessor->getValue($this->formValues, $propertyPath); + + if (!\is_array($data) || !isset($data[$index]) || !isset($data[$index - 1])) { + return; + } + + $formConfig = $this->getForm()->get($name)->getConfig(); + $orderPropertyPath = $formConfig->getOption('order_property_path'); + + if (null !== $orderPropertyPath) { + // Swap positions using the specified order property + $prevItemOrder = $propertyAccessor->getValue($data[$index - 1], $orderPropertyPath); + $currentItemOrder = $propertyAccessor->getValue($data[$index], $orderPropertyPath); + + $propertyAccessor->setValue($data[$index], $orderPropertyPath, $prevItemOrder); + $propertyAccessor->setValue($data[$index - 1], $orderPropertyPath, $currentItemOrder); + } else { + // Legacy behavior - simple array swap (with warning about limitations) + $temp = $data[$index - 1]; + $data[$index - 1] = $data[$index]; + $data[$index] = $temp; + } + + $propertyAccessor->setValue($this->formValues, $propertyPath, $data); + } + + #[LiveAction] + public function moveCollectionItemDown(PropertyAccessorInterface $propertyAccessor, #[LiveArg] string $name, #[LiveArg] int $index): void + { + $propertyPath = $this->fieldNameToPropertyPath($name, $this->formName); + $data = $propertyAccessor->getValue($this->formValues, $propertyPath); + + if (!\is_array($data) || !isset($data[$index]) || !isset($data[$index + 1])) { + return; + } + + $formConfig = $this->getForm()->get($name)->getConfig(); + $orderPropertyPath = $formConfig->getOption('order_property_path'); + + if (null !== $orderPropertyPath) { + // Swap positions using the specified order property + $nextItemOrder = $propertyAccessor->getValue($data[$index + 1], $orderPropertyPath); + $currentItemOrder = $propertyAccessor->getValue($data[$index], $orderPropertyPath); + + $propertyAccessor->setValue($data[$index], $orderPropertyPath, $nextItemOrder); + $propertyAccessor->setValue($data[$index + 1], $orderPropertyPath, $currentItemOrder); + } else { + // Legacy behavior - simple array swap (with warning about limitations) + $temp = $data[$index + 1]; + $data[$index + 1] = $data[$index]; + $data[$index] = $temp; + } + + $propertyAccessor->setValue($this->formValues, $propertyPath, $data); + } + private function fieldNameToPropertyPath(string $collectionFieldName, string $rootFormName): string { $propertyPath = $collectionFieldName; diff --git a/src/LiveComponent/templates/form_theme.html.twig b/src/LiveComponent/templates/form_theme.html.twig index 1042510f2f9..cc6222a094e 100644 --- a/src/LiveComponent/templates/form_theme.html.twig +++ b/src/LiveComponent/templates/form_theme.html.twig @@ -6,8 +6,14 @@ {%- endblock live_collection_widget -%} {%- block live_collection_entry_row -%} - {{ block('form_row') }} + {%- if button_move_up is defined and not button_move_up.rendered -%} + {{ form_row(button_move_up) }} + {%- endif -%} + {%- if button_move_down is defined and not button_move_down.rendered -%} + {{ form_row(button_move_down) }} + {%- endif -%} {%- if button_delete is defined and not button_delete.rendered -%} {{ form_row(button_delete) }} {%- endif -%} + {{ block('form_row') }} {%- endblock live_collection_entry_row -%}