diff --git a/core/modules/layout_builder/css/layout-builder.css b/core/modules/layout_builder/css/layout-builder.css index 1e7fea35a4..ff443e5b78 100644 --- a/core/modules/layout_builder/css/layout-builder.css +++ b/core/modules/layout_builder/css/layout-builder.css @@ -239,7 +239,7 @@ padding: 0 3px; } #drupal-off-canvas .layout-overview td .dropbutton-single .dropbutton-widget .dropbutton a { - margin-top: 0px; + margin-top: 0; color: #fff; } #drupal-off-canvas .layout-overview td .dropbutton-single .dropbutton-widget .dropbutton .dropbutton-action { diff --git a/core/modules/layout_builder/src/Controller/OverviewController.php b/core/modules/layout_builder/src/Controller/OverviewController.php index 841a855d55..3fbb770256 100644 --- a/core/modules/layout_builder/src/Controller/OverviewController.php +++ b/core/modules/layout_builder/src/Controller/OverviewController.php @@ -88,7 +88,6 @@ protected function getComponentOperations(SectionStorageInterface $section_stora 'data-dialog-renderer' => 'off_canvas', ], ]; - // @todo Add "move" operation https://www.drupal.org/project/drupal/issues/2995689. $operations['remove'] = [ 'title' => $this->t('Remove'), 'url' => Url::fromRoute('layout_builder.remove_block', $route_parameters, $this->getOverviewOptions(TRUE)), @@ -98,6 +97,15 @@ protected function getComponentOperations(SectionStorageInterface $section_stora 'data-dialog-renderer' => 'off_canvas', ], ]; + $operations['move'] = [ + 'title' => $this->t('Move'), + 'url' => Url::fromRoute('layout_builder.move_block_form', $route_parameters, $this->getOverviewOptions(TRUE)), + 'attributes' => [ + 'class' => ['use-ajax', 'move-block'], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', + ], + ]; return $operations; } @@ -152,7 +160,7 @@ protected function getSectionOperations(SectionStorageInterface $section_storage * The section storage. * @param int $delta * The delta of the section. - * @param $region + * @param string $region * The region. * * @return array @@ -251,6 +259,7 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s '#attributes' => [ 'class' => ['region-title'], 'no_striping' => TRUE, + 'data-section-delta' => $delta, ], 'label' => [ '#markup' => $layout_definition->getLabel(), @@ -270,6 +279,9 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s 'title' => [ '#markup' => $layout_definition->getRegionLabels()[$region], ], + '#attributes' => [ + 'data-section-region-delta' => "$delta|$region", + ], 'operations' => [ '#type' => 'operations', '#links' => $this->getRegionOperations($section_storage, $delta, $region), @@ -290,9 +302,13 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s $label = '@todo'; } + $plugin_id = $component->getPluginId(); $build["section-$delta-region-$region-uuid-$uuid"] = [ 'label' => [ - '#markup' => $label . ' (' . $component->getPluginId() . ')', + '#markup' => "$label ($plugin_id)", + ], + '#attributes' => [ + 'data-section-region-block-delta' => "$delta|$region|$plugin_id", ], 'operations' => [ '#type' => 'operations', diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index f71afd4c1a..c98f1fcb8e 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -2,7 +2,6 @@ namespace Drupal\layout_builder\Element; -use Drupal\Component\Serialization\Json; use Drupal\Core\Ajax\AjaxHelperTrait; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php index e8cf15f407..d7aeab204e 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlockFormTest.php @@ -13,6 +13,7 @@ class MoveBlockFormTest extends WebDriverTestBase { use ContextualLinkClickTrait; + use MoveBlocksTrait; /** * Path prefix for the field UI for the test bundle. @@ -71,6 +72,7 @@ protected function setUp() { $assert_session->assertWaitOnAjaxRequest(); $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'input[value="Add section"]')); $page->pressButton('Add section'); + $assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas'); $this->assertRegionBlocksOrder(1, 'content', $expected_block_order); // Add a 'Powered by Drupal' block in the 'first' region of the new section. @@ -93,13 +95,14 @@ protected function setUp() { /** * Tests moving a block. */ - public function testMoveBlock() { + public function testMAoveBlock() { $page = $this->getSession()->getPage(); // Reorder body field in current region. $this->openBodyMoveForm(1, 'content', ['Links', 'Body (current)']); $this->moveBlockWithKeyboard('up', 'Body (current)', ['Body (current)*', 'Links']); $page->pressButton('Move'); + $this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas'); $expected_block_order = [ '.block-field-blocknodebundle-with-section-fieldbody', '.block-extra-field-blocknodebundle-with-section-fieldlinks', @@ -115,6 +118,7 @@ public function testMoveBlock() { $this->assertBlockTable(['Powered by Drupal', 'Body (current)']); $this->moveBlockWithKeyboard('up', 'Body', ['Body (current)*', 'Powered by Drupal']); $page->pressButton('Move'); + $this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas'); $expected_block_order = [ '.block-field-blocknodebundle-with-section-fieldbody', '.block-system-powered-by-block', @@ -132,116 +136,8 @@ public function testMoveBlock() { $page->selectFieldOption('Region', '0:second'); $this->assertBlockTable(['Body (current)']); $page->pressButton('Move'); + $this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas'); $this->assertRegionBlocksOrder(0, 'second', ['.block-field-blocknodebundle-with-section-fieldbody']); } - /** - * Asserts the correct block labels appear in the draggable tables. - * - * @param string[] $expected_block_labels - * The expected block labels. - */ - protected function assertBlockTable(array $expected_block_labels) { - $page = $this->getSession()->getPage(); - $this->assertSession()->assertWaitOnAjaxRequest(); - $block_tds = $page->findAll('css', '.layout-builder-components-table__block-label'); - $this->assertCount(count($block_tds), $expected_block_labels); - /** @var \Behat\Mink\Element\NodeElement $block_td */ - foreach ($block_tds as $block_td) { - $this->assertSame(array_shift($expected_block_labels), trim($block_td->getText())); - } - } - - /** - * Moves a block in the draggable table. - * - * @param string $direction - * The direction to move the block in the table. - * @param string $block_label - * The block label. - * @param array $updated_blocks - * The updated blocks order. - */ - protected function moveBlockWithKeyboard($direction, $block_label, array $updated_blocks) { - $keys = [ - 'up' => 38, - 'down' => 40, - ]; - $key = $keys[$direction]; - $handle = $this->findRowHandle($block_label); - - $handle->keyDown($key); - $handle->keyUp($key); - - $handle->blur(); - $this->assertBlockTable($updated_blocks); - } - - /** - * Finds the row handle for a block in the draggable table. - * - * @param string $block_label - * The block label. - * - * @return \Behat\Mink\Element\NodeElement - * The row handle element. - */ - protected function findRowHandle($block_label) { - $assert_session = $this->assertSession(); - return $assert_session->elementExists('css', "[data-drupal-selector=\"edit-components\"] td:contains(\"$block_label\") a.tabledrag-handle"); - } - - /** - * Asserts that blocks are in the correct order for a region. - * - * @param int $section_delta - * The section delta. - * @param string $region - * The region. - * @param array $expected_block_selectors - * The block selectors. - */ - protected function assertRegionBlocksOrder($section_delta, $region, array $expected_block_selectors) { - $page = $this->getSession()->getPage(); - $assert_session = $this->assertSession(); - - $assert_session->assertWaitOnAjaxRequest(); - $assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas'); - - $region_selector = "[data-layout-delta=\"$section_delta\"] [data-region=\"$region\"]"; - - // Get all blocks currently in the region. - $blocks = $page->findAll('css', "$region_selector [data-layout-block-uuid]"); - $this->assertCount(count($expected_block_selectors), $blocks); - - /** @var \Behat\Mink\Element\NodeElement $block */ - foreach ($blocks as $block) { - $block_selector = array_shift($expected_block_selectors); - $assert_session->elementsCount('css', "$region_selector $block_selector", 1); - $expected_block = $page->find('css', "$region_selector $block_selector"); - $this->assertSame($expected_block->getAttribute('data-layout-block-uuid'), $block->getAttribute('data-layout-block-uuid')); - } - } - - /** - * Open block for the body field. - * - * @param int $delta - * The section delta where the field should be. - * @param string $region - * The region where the field should be. - * @param array $initial_blocks - * The initial blocks that should be shown in the draggable table. - */ - protected function openBodyMoveForm($delta, $region, array $initial_blocks) { - $assert_session = $this->assertSession(); - - $body_field_locator = "[data-layout-delta=\"$delta\"] [data-region=\"$region\"] .block-field-blocknodebundle-with-section-fieldbody"; - $this->clickContextualLink($body_field_locator, 'Move'); - $assert_session->assertWaitOnAjaxRequest(); - $this->assertNotEmpty($assert_session->waitForElementVisible('named', ['select', 'Region'])); - $assert_session->fieldValueEquals('Region', "$delta:$region"); - $this->assertBlockTable($initial_blocks); - } - } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlocksTrait.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlocksTrait.php new file mode 100644 index 0000000000..290808d90f --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/MoveBlocksTrait.php @@ -0,0 +1,115 @@ + 38, + 'down' => 40, + ]; + $key = $keys[$direction]; + $handle = $this->findRowHandle($block_label); + + $handle->keyDown($key); + $handle->keyUp($key); + + $handle->blur(); + $this->assertBlockTable($updated_blocks); + } + + /** + * Finds the row handle for a block in the draggable table. + * + * @param string $block_label + * The block label. + * + * @return \Behat\Mink\Element\NodeElement + * The row handle element. + */ + protected function findRowHandle($block_label) { + $assert_session = $this->assertSession(); + return $assert_session->elementExists('css', "[data-drupal-selector=\"edit-components\"] td:contains(\"$block_label\") a.tabledrag-handle"); + } + + /** + * Asserts that blocks are in the correct order for a region. + * + * @param int $section_delta + * The section delta. + * @param string $region + * The region. + * @param array $expected_block_selectors + * The block selectors. + */ + protected function assertRegionBlocksOrder($section_delta, $region, array $expected_block_selectors) { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + $region_selector = "[data-layout-delta=\"$section_delta\"] [data-region=\"$region\"]"; + + // Get all blocks currently in the region. + $blocks = $page->findAll('css', "$region_selector [data-layout-block-uuid]"); + $this->assertCount(count($expected_block_selectors), $blocks); + + /** @var \Behat\Mink\Element\NodeElement $block */ + foreach ($blocks as $block) { + $block_selector = array_shift($expected_block_selectors); + $assert_session->elementsCount('css', "$region_selector $block_selector", 1); + $expected_block = $page->find('css', "$region_selector $block_selector"); + $this->assertSame($expected_block->getAttribute('data-layout-block-uuid'), $block->getAttribute('data-layout-block-uuid')); + } + } + + /** + * Open block for the body field. + * + * @param int $delta + * The section delta where the field should be. + * @param string $region + * The region where the field should be. + * @param array $initial_blocks + * The initial blocks that should be shown in the draggable table. + */ + protected function openBodyMoveForm($delta, $region, array $initial_blocks) { + $assert_session = $this->assertSession(); + + $body_field_locator = "[data-layout-delta=\"$delta\"] [data-region=\"$region\"] .block-field-blocknodebundle-with-section-fieldbody"; + $this->clickContextualLink($body_field_locator, 'Move'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertNotEmpty($assert_session->waitForElementVisible('named', ['select', 'Region'])); + $assert_session->fieldValueEquals('Region', "$delta:$region"); + $this->assertBlockTable($initial_blocks); + } + + /** + * Asserts the correct block labels appear in the draggable tables. + * + * @param string[] $expected_block_labels + * The expected block labels. + */ + protected function assertBlockTable(array $expected_block_labels) { + $page = $this->getSession()->getPage(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $block_tds = $page->findAll('css', '.layout-builder-components-table__block-label'); + $this->assertCount(count($block_tds), $expected_block_labels); + /** @var \Behat\Mink\Element\NodeElement $block_td */ + foreach ($block_tds as $block_td) { + $this->assertSame(array_shift($expected_block_labels), trim($block_td->getText())); + } + } +} diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/OverviewUITest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/OverviewUITest.php new file mode 100644 index 0000000000..226359589f --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/OverviewUITest.php @@ -0,0 +1,246 @@ +createContentType(['type' => 'bundle_with_section_field']); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'create and edit custom blocks', + 'administer node display', + 'administer node fields', + 'access contextual links', + ])); + + // Enable layout builder. + $this->drupalPostForm( + static::FIELD_UI_PREFIX . '/display/default', + ['layout[enabled]' => TRUE], + 'Save' + ); + } + + /** + * Perform layout operations via overview dialog. + */ + public function testOperationsWithOverview() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout'); + + $this->openOverview(); + + $this->addSection('Two column', [ + 'configure_section' => [ + 'options' => [ + 'layout_settings[column_widths]' => '33-67', + ], + ], + ]); + + $this->addBlock(0, 'first', 'Powered by Drupal', '.block-system-powered-by-block'); + $this->addBlock(0, 'second', 'Changed', '.block-system-powered-by-block'); + + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '.layout--twocol-section--33-67')); + + $this->dropbuttonTask(0, 'first', 'system_powered_by_block', 'remove'); + $assert_session->assertNoElementAfterWait('css', '.block-system-powered-by-block'); + $this->dropbuttonTask(1, 'content', 'extra_field_block:node:bundle_with_section_field:links', 'configure', FALSE); + $page->fillField('settings[label]', 'Gold Star For Robot Boy'); + $page->checkField('settings[label_display]'); + $page->pressButton('Update'); + + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas #blocks')); + $this->assertTrue($assert_session->waitForText('Gold Star For Robot Boy')); + + $expected_block_order = [ + '.block-extra-field-blocknodebundle-with-section-fieldlinks', + '.block-field-blocknodebundle-with-section-fieldbody', + ]; + $this->assertRegionBlocksOrder(1, 'content', $expected_block_order); + $this->dropbuttonTask(1, 'content', 'extra_field_block:node:bundle_with_section_field:links', 'move', FALSE); + $this->moveBlockWithKeyboard('down', 'Gold Star For Robot Boy (current)', ['Body', 'Gold Star For Robot Boy (current)*']); + $page->pressButton('Move'); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas #blocks')); + $expected_block_order = [ + '.block-field-blocknodebundle-with-section-fieldbody', + '.block-extra-field-blocknodebundle-with-section-fieldlinks', + ]; + $this->assertRegionBlocksOrder(1, 'content', $expected_block_order); + } + + /** + * Opens the overview dialog. + */ + protected function openOverview() { + $this->getSession()->getPage()->clickLink('Layout overview'); + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas #blocks')); + } + + /** + * Add a section via the overview dialog. + * + * @param string $layout + * The layout the section should use. + * @param array|bool $options + * If the layout has options, set to true or an array with fields/values. + * @param int $delta + * Where to add the section. + */ + protected function addSection($layout, $options = [], $delta = 0) { + $number_of_sections_before = count($this->getSession()->getPage()->findAll('css', '.layout-builder__section')); + $add_section_links = $this->getSession()->getPage()->findAll('css', '#drupal-off-canvas #blocks .new-section__link'); + $this->assertCount(2, $add_section_links); + $add_section_link = $add_section_links[$delta]; + $add_section_link->click(); + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('named', ['link', $layout])); + + $this->clickLink($layout); + if (!empty($options['configure_section'])) { + $add_section_confirm_button = $this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas [value="Add section"]'); + $this->assertNotEmpty($add_section_confirm_button); + if (isset($options['configure_section']['options'])) { + foreach ($options['configure_section']['options'] as $field => $value) { + $this->getSession()->getPage()->findField($field)->setValue($value); + } + } + $add_section_confirm_button->click(); + } + + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas #blocks')); + $number_of_sections_after = count($this->getSession()->getPage()->findAll('css', '.layout-builder__section')); + $this->assertEqual($number_of_sections_after, $number_of_sections_before + 1); + + } + + /** + * Removes a section via the overview dialog. + * + * @param int $delta + * The section to remove. + * @param bool $confirm_remove + * Set to FALSE for this to stop at the confirmation dialog. + */ + protected function removeSection($delta, $confirm_remove = TRUE) { + $remove_button = $this->getSession()->getPage()->find('css', "[data-section-delta='$delta'] .remove-section"); + $this->assertNotEmpty($remove_button); + $remove_button->press(); + $this->assertTrue($this->assertSession()->waitForText('Are you sure you want to remove section')); + if ($confirm_remove) { + $this->getSession()->getPage()->pressButton('Remove'); + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas #blocks')); + + } + } + + /** + * Add a block via the overview dialog. + * + * @param int $delta + * The section where the block is added. + * @param string $region + * The region within the section where the block is added. + * @param string $block_name + * The name of the block to add. + * @param string $locator + * Optional - a css locator to assert exists after block is added. + */ + protected function addBlock($delta, $region, $block_name, $locator = '') { + $add_button = $this->getSession()->getPage()->find('css', "[data-section-region-delta='$delta|$region'] .add-block a"); + $this->assertNotEmpty($add_button); + $add_button->press(); + $this->assertTrue($this->assertSession()->waitForText('Choose a block')); + $this->getSession()->getPage()->clickLink($block_name); + $this->assertTrue($this->assertSession()->waitForText('Add block')); + $add_block_confirm_button = $this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas [value="Add block"]'); + $this->assertNotEmpty($add_block_confirm_button); + $add_block_confirm_button->press(); + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas #blocks')); + if (!empty($locator)) { + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', $locator)); + } + } + + /** + * Clicks a dropbutton for a block in the overview dialog. + * + * @param int $delta + * The section where the block is present. + * @param string $region + * The region within the section where the block is present. + * @param string $plugin_id + * The plugin id of the block. + * @param string $task + * The dropbutton task to choose. + * @param bool $submit_with_defaults + * When true, the task will be completed automatically. When false, the + * this method will complete with the task's config form open. + */ + protected function dropbuttonTask($delta, $region, $plugin_id, $task, $submit_with_defaults = TRUE) { + $block_locator = "[data-section-region-block-delta='$delta|$region|$plugin_id']"; + $dropbutton = $this->getSession()->getPage()->find('css', "$block_locator .dropbutton-toggle button"); + $this->assertNotEmpty($dropbutton); + $dropbutton->press(); + $open_dropbutton = $this->assertSession()->waitForElementVisible('css', "$block_locator .open"); + $this->assertNotEmpty($open_dropbutton); + $task_link = $open_dropbutton->find('css', ".$task-block"); + $this->assertNotEmpty($task_link); + $task_link->click(); + switch ($task) { + case 'configure': + $submit_button = $this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas [value="Update"]'); + break; + + case 'remove': + $submit_button = $this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas [value="Remove"]'); + break; + + case 'move': + $submit_button = $this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas [value="Move"]'); + break; + } + + // Use instead of assertNotEmpty as variable could potentially not exist. + $this->assertTrue(!empty($submit_button)); + + if ($submit_with_defaults) { + $submit_button->press(); + $this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-off-canvas #blocks')); + } + } + +} diff --git a/core/themes/stable/css/layout_builder/layout-builder.css b/core/themes/stable/css/layout_builder/layout-builder.css index 06b32a0eb4..a7aeb565ae 100644 --- a/core/themes/stable/css/layout_builder/layout-builder.css +++ b/core/themes/stable/css/layout_builder/layout-builder.css @@ -219,3 +219,35 @@ .layout-builder-components-table .tabledrag-changed-warning { display: none !important; } + +.region-title__action { + display: inline-block; + margin-left: 1em; /* LTR */ +} +[dir="rtl"] .region-title__action { + margin-left: 0; + margin-right: 1em; +} + +/* @todo Open an issue to fix .dropbutton-single in off-canvas reset */ +#drupal-off-canvas .layout-overview td .dropbutton-single .dropbutton-widget .dropbutton { + width: auto; + margin-left: 5px; + margin-right: 5px; +} +#drupal-off-canvas .layout-overview td .dropbutton-single .dropbutton-widget { + padding: 0 3px; +} +#drupal-off-canvas .layout-overview td .dropbutton-single .dropbutton-widget .dropbutton a { + margin-top: 0; + color: #fff; +} +#drupal-off-canvas .layout-overview td .dropbutton-single .dropbutton-widget .dropbutton .dropbutton-action { + background: transparent; +} + +.layout-overview .new-section { + margin-bottom: auto; + padding: 2px 0; + outline: none; +}