diff --git a/features/admin/page/inserting_content_elements_on_page.feature b/features/admin/page/inserting_content_elements_on_page.feature new file mode 100644 index 00000000..3154617d --- /dev/null +++ b/features/admin/page/inserting_content_elements_on_page.feature @@ -0,0 +1,35 @@ +@managing_pages +Feature: Inserting content elements between existing elements on a page + In order to manage the structure of content on a page + As an Administrator + I want to be able to insert content elements between existing ones + + Background: + Given I am logged in as an administrator + And the store operates on a single channel in "United States" + + @ui @javascript + Scenario: Inserting a content element between two existing elements + When I go to the create page page + And I fill the code with "insert-test-page" + And I fill the name with "Insert Test Page" + And I fill the slug with "insert-test-page" + And I add a heading content element with type "h1" and "My Title" content + And I add a textarea content element with "My body text" content + When I insert a textarea content element after the 1st content element + Then the 1st content element should be a "Heading" element + And the 2nd content element should be a "Textarea" element + And the 3rd content element should be a "Textarea" element + + @ui @javascript + Scenario: Inserting a content element before the first element + When I go to the create page page + And I fill the code with "insert-test-page" + And I fill the name with "Insert Test Page" + And I fill the slug with "insert-test-page" + And I add a heading content element with type "h1" and "My Title" content + And I add a textarea content element with "My body text" content + When I insert a textarea content element before the 1st content element + Then the 1st content element should be a "Textarea" element + And the 2nd content element should be a "Heading" element + And the 3rd content element should be a "Textarea" element diff --git a/features/admin/page/sorting_content_elements_on_page.feature b/features/admin/page/sorting_content_elements_on_page.feature index aaac0dfb..b971d4c9 100644 --- a/features/admin/page/sorting_content_elements_on_page.feature +++ b/features/admin/page/sorting_content_elements_on_page.feature @@ -51,3 +51,35 @@ Feature: Sorting content elements on a page And I add a heading content element with type "h1" and "My Title" content And I add a textarea content element with "My body text" content Then the move down button of the 2nd content element should be disabled + + @ui @javascript + Scenario: Moving a content element down keeps its content when editing an existing page + Given there is a page in the store with a textarea content element with "First content" content and a textarea content element with "Second content" content + When I want to edit this page + And I move the 1st content element down + Then the 1st content element should contain "Second content" + And the 2nd content element should contain "First content" + + @ui @javascript + Scenario: Moving a content element up keeps its content when editing an existing page + Given there is a page in the store with a textarea content element with "First content" content and a textarea content element with "Second content" content + When I want to edit this page + And I move the 2nd content element up + Then the 1st content element should contain "Second content" + And the 2nd content element should contain "First content" + + @ui @javascript @quill + Scenario: Moving a content element down keeps its content with the Quill editor + Given there is a page in the store with a textarea content element with "First content" content and a textarea content element with "Second content" content + When I want to edit this page + And I move the 1st content element down + Then the 1st content element should contain "Second content" + And the 2nd content element should contain "First content" + + @ui @javascript @quill + Scenario: Moving a content element up keeps its content with the Quill editor + Given there is a page in the store with a textarea content element with "First content" content and a textarea content element with "Second content" content + When I want to edit this page + And I move the 2nd content element up + Then the 1st content element should contain "Second content" + And the 2nd content element should contain "First content" diff --git a/src/Twig/Component/Trait/ContentElementsCollectionFormComponentTrait.php b/src/Twig/Component/Trait/ContentElementsCollectionFormComponentTrait.php index 5b189e56..8bba9eeb 100644 --- a/src/Twig/Component/Trait/ContentElementsCollectionFormComponentTrait.php +++ b/src/Twig/Component/Trait/ContentElementsCollectionFormComponentTrait.php @@ -70,10 +70,28 @@ public function moveCollectionItem( return; } - $swapKey = $keys[$swapPos]; - [$data[$index], $data[$swapKey]] = [$data[$swapKey], $data[$index]]; + $items = array_values($data); + [$items[$currentPos], $items[$swapPos]] = [$items[$swapPos], $items[$currentPos]]; + + // Give fresh keys to the two moved rows only, while keeping the visual (insertion) + // order. New keys mean new DOM ids, so the Live Component re-creates exactly those + // two rows instead of patching them in place. This keeps stateful WYSIWYG widgets + // correct regardless of their morphing strategy: Trix opts out of morphing via + // "data-live-ignore", while Quill builds its own DOM the server never renders - in + // both cases an in-place patch would leave stale or corrupted content. Re-creating + // the row triggers the editor's disconnect()/connect() cycle, which is the path it + // is built to support. Untouched rows keep their keys (and initialized editors). + $keys = array_keys($data); + $freshIndex = $this->provideNewCollectionItemIndex($data); + $keys[$currentPos] = $freshIndex; + $keys[$swapPos] = $freshIndex + 1; + + $reordered = []; + foreach ($items as $position => $item) { + $reordered[$keys[$position]] = $item; + } - $propertyAccessor->setValue($this->formValues, $propertyPath, $data); + $propertyAccessor->setValue($this->formValues, $propertyPath, $reordered); } #[LiveAction] @@ -88,6 +106,43 @@ public function applyContentTemplate(#[LiveArg] string $localeCode): void $this->populateElements($localeCode, $template); } + #[LiveAction] + public function insertCollectionItem( + PropertyAccessorInterface $propertyAccessor, + #[LiveArg] + string $name, + #[LiveArg] + ?string $type = null, + #[LiveArg] + ?int $insertAfterIndex = null, + ): void { + $propertyPath = $this->fieldNameToPropertyPath($name, $this->formName ?? ''); + $data = $propertyAccessor->getValue($this->formValues, $propertyPath); + + if (!\is_array($data)) { + $data = []; + } + + // Do not sort by key here: the collection is rendered in insertion order, not key + // order, and moveCollectionItem() intentionally produces non-monotonic keys. Sorting + // would scramble the visual order after a move. array_values() preserves it. + $values = array_values($data); + $newItem = null === $type ? [] : ['type' => $type]; + + if (null === $insertAfterIndex) { + $values[] = $newItem; + } elseif ($insertAfterIndex < 0) { + array_unshift($values, $newItem); + } else { + $keys = array_keys($data); + $pos = array_search($insertAfterIndex, $keys, true); + $insertPosition = false !== $pos ? $pos + 1 : count($values); + array_splice($values, $insertPosition, 0, [$newItem]); + } + + $propertyAccessor->setValue($this->formValues, $propertyPath, $values); + } + /** @param TemplateRepositoryInterface $templateRepository */ protected function initializeTemplateRepository(TemplateRepositoryInterface $templateRepository): void { diff --git a/templates/admin/macros/insert_element_divider.html.twig b/templates/admin/macros/insert_element_divider.html.twig new file mode 100644 index 00000000..2e84df50 --- /dev/null +++ b/templates/admin/macros/insert_element_divider.html.twig @@ -0,0 +1,21 @@ +{% macro insert_element_divider(collection_types, collection_name, insert_after_index) %} +
+
+ +
+
+{% endmacro %} diff --git a/templates/admin/shared/component_elements/form_theme.html.twig b/templates/admin/shared/component_elements/form_theme.html.twig index 1be47c66..87a28c89 100644 --- a/templates/admin/shared/component_elements/form_theme.html.twig +++ b/templates/admin/shared/component_elements/form_theme.html.twig @@ -1,7 +1,24 @@ {% extends '@SyliusAdmin/shared/form_theme.html.twig' %} {%- block live_collection_widget -%} - {{ block('form_widget') }} + {%- import '@SyliusCmsPlugin/admin/macros/insert_element_divider.html.twig' as InsertElementButton -%} + + {%- set collection_types = button_add is defined ? button_add.vars.types : {} -%} + {%- set collection_name = button_add is defined ? button_add.vars.attr['data-live-name-param'] : '' -%} + +
+ {%- for child in form -%} + {%- if loop.first and collection_types is not empty -%} + {{ InsertElementButton.insert_element_divider(collection_types, collection_name, -1) }} + {%- endif -%} + + {{ form_row(child) }} + + {%- if not loop.last and collection_types is not empty -%} + {{ InsertElementButton.insert_element_divider(collection_types, collection_name, child.vars.name) }} + {%- endif -%} + {%- endfor -%} +
{%- endblock live_collection_widget -%} {%- block live_collection_entry_row -%} @@ -57,12 +74,17 @@ {% block add_button_row %} {% if types is not empty %} -