diff --git a/core/modules/settings_tray/settings_tray.links.contextual.yml b/core/modules/settings_tray/settings_tray.links.contextual.yml index 5534ab2..c62fa98 100644 --- a/core/modules/settings_tray/settings_tray.links.contextual.yml +++ b/core/modules/settings_tray/settings_tray.links.contextual.yml @@ -1,6 +1,6 @@ settings_tray.block_configure: title: 'Quick edit' - route_name: 'entity.block.off_canvas_form' + route_name: 'entity.block.settings_tray_form' group: 'block' options: attributes: diff --git a/core/modules/settings_tray/settings_tray.module b/core/modules/settings_tray/settings_tray.module index 87b40ac..7a7d825 100644 --- a/core/modules/settings_tray/settings_tray.module +++ b/core/modules/settings_tray/settings_tray.module @@ -8,7 +8,7 @@ use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; -use Drupal\settings_tray\Block\BlockEntityOffCanvasForm; +use Drupal\settings_tray\Block\BlockEntitySettingTrayForm; use Drupal\block\entity\Block; use Drupal\block\BlockInterface; @@ -19,12 +19,12 @@ function settings_tray_help($route_name, RouteMatchInterface $route_match) { switch ($route_name) { case 'help.page.settings_tray': $output = '

' . t('About') . '

'; - $output .= '

' . t('The Settings Tray module allows users with the Administer blocks and Use contextual links permissions to edit blocks without visiting a separate page. For more information, see the online documentation for the Settings Tray module.', [':handbook_url' => 'https://www.drupal.org/documentation/modules/settings_tray', ':administer_block_permission' => \Drupal::url('user.admin_permissions', [], ['fragment' => 'module-block']), ':contextual_permission' => \Drupal::url('user.admin_permissions', [], ['fragment' => 'module-contextual'])]) . '

'; + $output .= '

' . t('The Settings Tray module allows users with the Administer blocks and Use contextual links permissions to edit blocks without visiting a separate page. For more information, see the online documentation for the Settings Tray module.', [':handbook_url' => 'https://www.drupal.org/documentation/modules/settings_tray', ':administer_block_permission' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-block']), ':contextual_permission' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-contextual'])]) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Editing blocks in place') . '
'; $output .= '
'; - $output .= '

' . t('To edit blocks in place, either click the Edit button in the toolbar and then click on the block, or choose "Quick edit" from the block\'s contextual link. (See the Contextual Links module help for more information about how to use contextual links.)', [':contextual' => \Drupal::url('help.page', ['name' => 'contextual'])]) . '

'; + $output .= '

' . t('To edit blocks in place, either click the Edit button in the toolbar and then click on the block, or choose "Quick edit" from the block\'s contextual link. (See the Contextual Links module help for more information about how to use contextual links.)', [':contextual' => Url::fromRoute('help.page', ['name' => 'contextual'])]) . '

'; $output .= '

' . t('The Settings Tray for the block will open in a sidebar, with a compact form for configuring what the block shows.') . '

'; $output .= '

' . t('Save the form and the changes will be immediately visible on the page.') . '

'; $output .= '
'; @@ -96,8 +96,8 @@ function settings_tray_block_view_alter(array &$build) { function settings_tray_entity_type_build(array &$entity_types) { /* @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ $entity_types['block'] - ->setFormClass('off_canvas', BlockEntityOffCanvasForm::class) - ->setLinkTemplate('off_canvas-form', '/admin/structure/block/manage/{block}/off-canvas'); + ->setFormClass('settings_tray', BlockEntitySettingTrayForm::class) + ->setLinkTemplate('settings_tray-form', '/admin/structure/block/manage/{block}/settings-tray'); } /** diff --git a/core/modules/settings_tray/settings_tray.routing.yml b/core/modules/settings_tray/settings_tray.routing.yml index f8e2bfe..370fc7f 100644 --- a/core/modules/settings_tray/settings_tray.routing.yml +++ b/core/modules/settings_tray/settings_tray.routing.yml @@ -1,9 +1,16 @@ -entity.block.off_canvas_form: - path: '/admin/structure/block/manage/{block}/off-canvas' +entity.block.settings_tray_form: + path: '/admin/structure/block/manage/{block}/settings-tray' defaults: - _entity_form: 'block.off_canvas' - _title_callback: '\Drupal\settings_tray\Block\BlockEntityOffCanvasForm::title' + _entity_form: 'block.settings_tray' + _title_callback: '\Drupal\settings_tray\Block\BlockEntitySettingTrayForm::title' requirements: _permission: 'administer blocks' _access_block_plugin_has_settings_tray_form: 'TRUE' _access_block_has_overrides_settings_tray_form: 'TRUE' + +# Deprecated. +# @see entity.block.settings_tray_form +# @see \Drupal\settings_tray\RouteProcessor\BlockEntityOffCanvasFormRouteProcessorBC +# @todo Remove in Drupal 9.0.0. +entity.block.off_canvas_form: + path: '' diff --git a/core/modules/settings_tray/settings_tray.services.yml b/core/modules/settings_tray/settings_tray.services.yml index 7c57e95..9f61546 100644 --- a/core/modules/settings_tray/settings_tray.services.yml +++ b/core/modules/settings_tray/settings_tray.services.yml @@ -7,3 +7,12 @@ services: class: Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck tags: - { name: access_check, applies_to: _access_block_plugin_has_settings_tray_form } + + # BC layers. + # @todo Remove in Drupal 9.0.0. + settings_tray.route_processor_off_canvas_form_bc: + class: \Drupal\settings_tray\RouteProcessor\BlockEntityOffCanvasFormRouteProcessorBC + arguments: ['@router.route_provider'] + public: false + tags: + - { name: route_processor_outbound } diff --git a/core/modules/settings_tray/src/Block/BlockEntityOffCanvasForm.php b/core/modules/settings_tray/src/Block/BlockEntityOffCanvasForm.php deleted file mode 100644 index 2c6f80d..0000000 --- a/core/modules/settings_tray/src/Block/BlockEntityOffCanvasForm.php +++ /dev/null @@ -1,194 +0,0 @@ - once - // https://www.drupal.org/node/2359901 is fixed. - return $this->t('Configure @block', ['@block' => $block->getPlugin()->getPluginDefinition()['admin_label']]); - } - - /** - * {@inheritdoc} - */ - public function form(array $form, FormStateInterface $form_state) { - $form = parent::form($form, $form_state); - - // Create link to full block form. - $query = []; - if ($destination = $this->getRequest()->query->get('destination')) { - $query['destination'] = $destination; - } - $form['advanced_link'] = [ - '#type' => 'link', - '#title' => $this->t('Advanced block options'), - '#url' => $this->entity->toUrl('edit-form', ['query' => $query]), - '#weight' => 1000, - ]; - - // Remove the ID and region elements. - unset($form['id'], $form['region'], $form['settings']['admin_label']); - - if (isset($form['settings']['label_display']) && isset($form['settings']['label'])) { - // Only show the label input if the label will be shown on the page. - $form['settings']['label_display']['#weight'] = -100; - $form['settings']['label']['#states']['visible'] = [ - ':input[name="settings[label_display]"]' => ['checked' => TRUE], - ]; - - // Relabel to "Block title" because on the front-end this may be confused - // with page title. - $form['settings']['label']['#title'] = $this->t("Block title"); - $form['settings']['label_display']['#title'] = $this->t("Display block title"); - } - return $form; - } - - /** - * {@inheritdoc} - */ - protected function actions(array $form, FormStateInterface $form_state) { - $actions = parent::actions($form, $form_state); - $actions['submit']['#value'] = $this->t('Save @block', ['@block' => $this->entity->getPlugin()->getPluginDefinition()['admin_label']]); - $actions['delete']['#access'] = FALSE; - return $actions; - } - - /** - * {@inheritdoc} - */ - protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) { - // Do not display the visibility. - return []; - } - - /** - * {@inheritdoc} - */ - protected function validateVisibility(array $form, FormStateInterface $form_state) { - // Intentionally empty. - } - - /** - * {@inheritdoc} - */ - protected function submitVisibility(array $form, FormStateInterface $form_state) { - // Intentionally empty. - } - - /** - * {@inheritdoc} - */ - protected function getPluginForm(BlockPluginInterface $block) { - if ($block instanceof PluginWithFormsInterface) { - return $this->pluginFormFactory->createInstance($block, 'settings_tray', 'configure'); - } - return $block; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $form = parent::buildForm($form, $form_state); - $form['actions']['submit']['#ajax'] = [ - 'callback' => '::submitFormDialog', - ]; - $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; - - // static::submitFormDialog() requires data-drupal-selector to be the same - // between the various Ajax requests. A bug in - // \Drupal\Core\Form\FormBuilder prevents that from happening unless - // $form['#id'] is also the same. Normally, #id is set to a unique HTML ID - // via Html::getUniqueId(), but here we bypass that in order to work around - // the data-drupal-selector bug. This is okay so long as we assume that this - // form only ever occurs once on a page. - // @todo Remove this workaround once https://www.drupal.org/node/2897377 is - // fixed. - $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); - - return $form; - } - - /** - * Submit form dialog #ajax callback. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return \Drupal\Core\Ajax\AjaxResponse - * An AJAX response that display validation error messages or redirects - * to a URL - * - * @todo Repalce this callback with generic trait in - * https://www.drupal.org/node/2896535. - */ - public function submitFormDialog(array &$form, FormStateInterface $form_state) { - $response = new AjaxResponse(); - if ($form_state->hasAnyErrors()) { - $form['status_messages'] = [ - '#type' => 'status_messages', - '#weight' => -1000, - ]; - $command = new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form); - } - else { - if ($redirect_url = $this->getRedirectUrl()) { - $command = new RedirectCommand($redirect_url->setAbsolute()->toString()); - } - else { - // Settings Tray always provides a destination. - throw new \Exception("No destination provided by Settings Tray form"); - } - } - return $response->addCommand($command); - } - - /** - * Gets the form's redirect URL from 'destination' provide in the request. - * - * @return \Drupal\Core\Url|null - * The redirect URL or NULL if dialog should just be closed. - */ - protected function getRedirectUrl() { - // \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly - // because it will use if 'destination' is not in the query - // string. - if ($this->getRequest()->query->has('destination') && $destination = $this->getRedirectDestination()->get()) { - return Url::fromUserInput('/' . $destination); - } - } - -} diff --git a/core/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php b/core/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php new file mode 100644 index 0000000..fd44b0c --- /dev/null +++ b/core/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php @@ -0,0 +1,194 @@ + once + // https://www.drupal.org/node/2359901 is fixed. + return $this->t('Configure @block', ['@block' => $block->getPlugin()->getPluginDefinition()['admin_label']]); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + // Create link to full block form. + $query = []; + if ($destination = $this->getRequest()->query->get('destination')) { + $query['destination'] = $destination; + } + $form['advanced_link'] = [ + '#type' => 'link', + '#title' => $this->t('Advanced block options'), + '#url' => $this->entity->toUrl('edit-form', ['query' => $query]), + '#weight' => 1000, + ]; + + // Remove the ID and region elements. + unset($form['id'], $form['region'], $form['settings']['admin_label']); + + if (isset($form['settings']['label_display']) && isset($form['settings']['label'])) { + // Only show the label input if the label will be shown on the page. + $form['settings']['label_display']['#weight'] = -100; + $form['settings']['label']['#states']['visible'] = [ + ':input[name="settings[label_display]"]' => ['checked' => TRUE], + ]; + + // Relabel to "Block title" because on the front-end this may be confused + // with page title. + $form['settings']['label']['#title'] = $this->t("Block title"); + $form['settings']['label_display']['#title'] = $this->t("Display block title"); + } + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Save @block', ['@block' => $this->entity->getPlugin()->getPluginDefinition()['admin_label']]); + $actions['delete']['#access'] = FALSE; + return $actions; + } + + /** + * {@inheritdoc} + */ + protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) { + // Do not display the visibility. + return []; + } + + /** + * {@inheritdoc} + */ + protected function validateVisibility(array $form, FormStateInterface $form_state) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + protected function submitVisibility(array $form, FormStateInterface $form_state) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + protected function getPluginForm(BlockPluginInterface $block) { + if ($block instanceof PluginWithFormsInterface) { + return $this->pluginFormFactory->createInstance($block, 'settings_tray', 'configure'); + } + return $block; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + $form['actions']['submit']['#ajax'] = [ + 'callback' => '::submitFormDialog', + ]; + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + + // static::submitFormDialog() requires data-drupal-selector to be the same + // between the various Ajax requests. A bug in + // \Drupal\Core\Form\FormBuilder prevents that from happening unless + // $form['#id'] is also the same. Normally, #id is set to a unique HTML ID + // via Html::getUniqueId(), but here we bypass that in order to work around + // the data-drupal-selector bug. This is okay so long as we assume that this + // form only ever occurs once on a page. + // @todo Remove this workaround once https://www.drupal.org/node/2897377 is + // fixed. + $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); + + return $form; + } + + /** + * Submit form dialog #ajax callback. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * An AJAX response that display validation error messages or redirects + * to a URL + * + * @todo Repalce this callback with generic trait in + * https://www.drupal.org/node/2896535. + */ + public function submitFormDialog(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + if ($form_state->hasAnyErrors()) { + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -1000, + ]; + $command = new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form); + } + else { + if ($redirect_url = $this->getRedirectUrl()) { + $command = new RedirectCommand($redirect_url->setAbsolute()->toString()); + } + else { + // Settings Tray always provides a destination. + throw new \Exception("No destination provided by Settings Tray form"); + } + } + return $response->addCommand($command); + } + + /** + * Gets the form's redirect URL from 'destination' provide in the request. + * + * @return \Drupal\Core\Url|null + * The redirect URL or NULL if dialog should just be closed. + */ + protected function getRedirectUrl() { + // \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly + // because it will use if 'destination' is not in the query + // string. + if ($this->getRequest()->query->has('destination') && $destination = $this->getRedirectDestination()->get()) { + return Url::fromUserInput('/' . $destination); + } + } + +} diff --git a/core/modules/settings_tray/src/RouteProcessor/BlockEntityOffCanvasFormRouteProcessorBC.php b/core/modules/settings_tray/src/RouteProcessor/BlockEntityOffCanvasFormRouteProcessorBC.php new file mode 100644 index 0000000..48935ae --- /dev/null +++ b/core/modules/settings_tray/src/RouteProcessor/BlockEntityOffCanvasFormRouteProcessorBC.php @@ -0,0 +1,65 @@ +routeProvider = $route_provider; + } + + /** + * {@inheritdoc} + */ + public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { + if ($route_name === 'entity.block.off_canvas_form') { + $redirected_route_name = 'entity.block.settings_tray_form'; + @trigger_error(sprintf("The '%s' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the '%s' route instead.", $route_name, $redirected_route_name), E_USER_DEPRECATED); + static::overwriteRoute($route, $this->routeProvider->getRouteByName($redirected_route_name)); + } + } + + /** + * Overwrites one route's metadata with the other's. + * + * @param \Symfony\Component\Routing\Route $target_route + * The route whose metadata to overwrite. + * @param \Symfony\Component\Routing\Route $source_route + * The route whose metadata to read from. + * + * @see \Symfony\Component\Routing\Route + */ + protected static function overwriteRoute(Route $target_route, Route $source_route) { + $target_route->setPath($source_route->getPath()); + $target_route->setDefaults($source_route->getDefaults()); + $target_route->setRequirements($source_route->getRequirements()); + $target_route->setOptions($source_route->getOptions()); + $target_route->setHost($source_route->getHost()); + $target_route->setSchemes($source_route->getSchemes()); + $target_route->setMethods($source_route->getMethods()); + } + +} diff --git a/core/modules/settings_tray/tests/src/Functional/BcRoutesTest.php b/core/modules/settings_tray/tests/src/Functional/BcRoutesTest.php new file mode 100644 index 0000000..6750b1c --- /dev/null +++ b/core/modules/settings_tray/tests/src/Functional/BcRoutesTest.php @@ -0,0 +1,33 @@ +placeBlock('system_powered_by_block'); + $url_for_current_route = Url::fromRoute('entity.block.settings_tray_form', ['block' => $block->id()])->toString(TRUE)->getGeneratedUrl(); + $url_for_bc_route = Url::fromRoute('entity.block.off_canvas_form', ['block' => $block->id()])->toString(TRUE)->getGeneratedUrl(); + $this->assertSame($url_for_current_route, $url_for_bc_route); + } + +} diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php index c677d08..7475dab 100644 --- a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php +++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php @@ -84,7 +84,7 @@ public function testBlocks($theme, $block_plugin, $new_page_text, $element_selec $link = $page->find('css', "$block_selector .contextual-links li a"); $this->assertEquals('Quick edit', $link->getText(), "'Quick edit' is the first contextual link for the block."); - $this->assertContains("/admin/structure/block/manage/$block_id/off-canvas?destination=user/2", $link->getAttribute('href')); + $this->assertContains("/admin/structure/block/manage/$block_id/settings-tray?destination=user/2", $link->getAttribute('href')); if (isset($toolbar_item)) { // Check that you can open a toolbar tray and it will be closed after @@ -525,7 +525,7 @@ public function testCustomBlockLinks() { $href = array_search('Quick edit', $link_labels); $this->assertEquals('', $href); $href = array_search('Quick edit settings', $link_labels); - $this->assertTrue(strstr($href, '/admin/structure/block/manage/custom/off-canvas?destination=user/2') !== FALSE); + $this->assertTrue(strstr($href, '/admin/structure/block/manage/custom/settings-tray?destination=user/2') !== FALSE); } /** diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php.orig b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php.orig new file mode 100644 index 0000000..c677d08 --- /dev/null +++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php.orig @@ -0,0 +1,720 @@ +createBlockContentType('basic', TRUE); + $block_content = $this->createBlockContent('Custom Block', 'basic', TRUE); + $user = $this->createUser([ + 'administer blocks', + 'access contextual links', + 'access toolbar', + 'administer nodes', + 'search content', + ]); + $this->drupalLogin($user); + $this->placeBlock('block_content:' . $block_content->uuid(), ['id' => 'custom']); + } + + /** + * Tests opening off-canvas dialog by click blocks and elements in the blocks. + * + * @dataProvider providerTestBlocks + */ + public function testBlocks($theme, $block_plugin, $new_page_text, $element_selector, $label_selector, $button_text, $toolbar_item) { + $web_assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->enableTheme($theme); + $block = $this->placeBlock($block_plugin); + $block_selector = $this->getBlockSelector($block); + $block_id = $block->id(); + $this->drupalGet('user'); + + $link = $page->find('css', "$block_selector .contextual-links li a"); + $this->assertEquals('Quick edit', $link->getText(), "'Quick edit' is the first contextual link for the block."); + $this->assertContains("/admin/structure/block/manage/$block_id/off-canvas?destination=user/2", $link->getAttribute('href')); + + if (isset($toolbar_item)) { + // Check that you can open a toolbar tray and it will be closed after + // entering edit mode. + if ($element = $page->find('css', "#toolbar-administration a.is-active")) { + // If a tray was open from page load close it. + $element->click(); + $this->waitForNoElement("#toolbar-administration a.is-active"); + } + $page->find('css', $toolbar_item)->click(); + $this->assertElementVisibleAfterWait('css', "{$toolbar_item}.is-active"); + } + $this->enableEditMode(); + if (isset($toolbar_item)) { + $this->waitForNoElement("{$toolbar_item}.is-active"); + } + $this->openBlockForm($block_selector); + switch ($block_plugin) { + case 'system_powered_by_block': + // Confirm "Display Title" is not checked. + $web_assert->checkboxNotChecked('settings[label_display]'); + // Confirm Title is not visible. + $this->assertEquals($this->isLabelInputVisible(), FALSE, 'Label is not visible'); + $page->checkField('settings[label_display]'); + $this->assertEquals($this->isLabelInputVisible(), TRUE, 'Label is visible'); + // Fill out form, save the form. + $page->fillField('settings[label]', $new_page_text); + + break; + + case 'system_branding_block': + // Fill out form, save the form. + $page->fillField('settings[site_information][site_name]', $new_page_text); + break; + + case 'settings_tray_test_class': + $web_assert->elementExists('css', '[data-drupal-selector="edit-settings-some-setting"]'); + break; + } + + if (isset($new_page_text)) { + $page->pressButton($button_text); + // Make sure the changes are present. + $new_page_text_locator = "$block_selector $label_selector:contains($new_page_text)"; + $this->assertElementVisibleAfterWait('css', $new_page_text_locator); + // The page is loaded with the new change but make sure page is + // completely loaded. + $this->assertPageLoadComplete(); + } + + $this->openBlockForm($block_selector); + + $this->disableEditMode(); + // Canvas should close when editing module is closed. + $this->waitForOffCanvasToClose(); + + $this->enableEditMode(); + + // Open block form by clicking a element inside the block. + // This confirms that default action for links and form elements is + // suppressed. + $this->openBlockForm("$block_selector {$element_selector}", $block_selector); + $web_assert->elementTextContains('css', '.contextual-toolbar-tab button', 'Editing'); + $web_assert->elementAttributeContains('css', '.dialog-off-canvas-main-canvas', 'class', 'js-settings-tray-edit-mode'); + // Simulate press the Escape key. + $this->getSession()->executeScript('jQuery("body").trigger(jQuery.Event("keyup", { keyCode: 27 }));'); + $this->waitForOffCanvasToClose(); + $this->getSession()->wait(100); + $this->assertEditModeDisabled(); + $web_assert->elementTextContains('css', '#drupal-live-announce', 'Exited edit mode.'); + $web_assert->elementTextNotContains('css', '.contextual-toolbar-tab button', 'Editing'); + $web_assert->elementAttributeNotContains('css', '.dialog-off-canvas-main-canvas', 'class', 'js-settings-tray-edit-mode'); + } + + /** + * Dataprovider for testBlocks(). + */ + public function providerTestBlocks() { + $blocks = []; + foreach ($this->getTestThemes() as $theme) { + $blocks += [ + "$theme: block-powered" => [ + 'theme' => $theme, + 'block_plugin' => 'system_powered_by_block', + 'new_page_text' => 'Can you imagine anyone showing the label on this block', + 'element_selector' => 'span a', + 'label_selector' => 'h2', + 'button_text' => 'Save Powered by Drupal', + 'toolbar_item' => '#toolbar-item-user', + ], + "$theme: block-branding" => [ + 'theme' => $theme, + 'block_plugin' => 'system_branding_block', + 'new_page_text' => 'The site that will live a very short life', + 'element_selector' => "a[rel='home']:last-child", + 'label_selector' => "a[rel='home']:last-child", + 'button_text' => 'Save Site branding', + 'toolbar_item' => '#toolbar-item-administration', + ], + "$theme: block-search" => [ + 'theme' => $theme, + 'block_plugin' => 'search_form_block', + 'new_page_text' => NULL, + 'element_selector' => '#edit-submit', + 'label_selector' => 'h2', + 'button_text' => 'Save Search form', + 'toolbar_item' => NULL, + ], + // This is the functional JS test coverage accompanying + // \Drupal\Tests\settings_tray\Functional\SettingsTrayTest::testPossibleAnnotations(). + "$theme: " . SettingsTrayFormAnnotationIsClassBlock::class => [ + 'theme' => $theme, + 'block_plugin' => 'settings_tray_test_class', + 'new_page_text' => NULL, + 'element_selector' => 'span', + 'label_selector' => NULL, + 'button_text' => NULL, + 'toolbar_item' => NULL, + ], + // This is the functional JS test coverage accompanying + // \Drupal\Tests\settings_tray\Functional\SettingsTrayTest::testPossibleAnnotations(). + "$theme: " . SettingsTrayFormAnnotationNoneBlock::class => [ + 'theme' => $theme, + 'block_plugin' => 'settings_tray_test_none', + 'new_page_text' => NULL, + 'element_selector' => 'span', + 'label_selector' => NULL, + 'button_text' => NULL, + 'toolbar_item' => NULL, + ], + ]; + } + + return $blocks; + } + + /** + * Enables edit mode by pressing edit button in the toolbar. + */ + protected function enableEditMode() { + $this->pressToolbarEditButton(); + $this->assertEditModeEnabled(); + } + + /** + * Disables edit mode by pressing edit button in the toolbar. + */ + protected function disableEditMode() { + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->pressToolbarEditButton(); + $this->assertEditModeDisabled(); + } + + /** + * Asserts that Off-Canvas block form is valid. + */ + protected function assertOffCanvasBlockFormIsValid() { + $web_assert = $this->assertSession(); + // Confirm that Block title display label has been changed. + $web_assert->elementTextContains('css', '.form-item-settings-label-display label', 'Display block title'); + // Confirm Block title label is shown if checkbox is checked. + if ($this->getSession()->getPage()->find('css', 'input[name="settings[label_display]"]')->isChecked()) { + $this->assertEquals($this->isLabelInputVisible(), TRUE, 'Label is visible'); + $web_assert->elementTextContains('css', '.form-item-settings-label label', 'Block title'); + } + else { + $this->assertEquals($this->isLabelInputVisible(), FALSE, 'Label is not visible'); + } + + // Check that common block form elements exist. + $web_assert->elementExists('css', static::LABEL_INPUT_SELECTOR); + $web_assert->elementExists('css', 'input[data-drupal-selector="edit-settings-label-display"]'); + // Check that advanced block form elements do not exist. + $web_assert->elementNotExists('css', 'input[data-drupal-selector="edit-visibility-request-path-pages"]'); + $web_assert->elementNotExists('css', 'select[data-drupal-selector="edit-region"]'); + } + + /** + * Open block form by clicking the element found with a css selector. + * + * @param string $block_selector + * A css selector selects the block or an element within it. + * @param string $contextual_link_container + * The element that contains the contextual links. If none provide the + * $block_selector will be used. + */ + protected function openBlockForm($block_selector, $contextual_link_container = '') { + if (!$contextual_link_container) { + $contextual_link_container = $block_selector; + } + // Ensure that contextual link element is present because this is required + // to open the off-canvas dialog in edit mode. + $contextual_link = $this->assertSession()->waitForElement('css', "$contextual_link_container .contextual-links a"); + $this->assertNotEmpty($contextual_link); + // When page first loads Edit Mode is not triggered until first contextual + // link is added. + $this->assertElementVisibleAfterWait('css', '.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode'); + // Ensure that all other Ajax activity is completed. + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->click($block_selector); + $this->waitForOffCanvasToOpen(); + $this->assertOffCanvasBlockFormIsValid(); + } + + /** + * Tests QuickEdit links behavior. + */ + public function testQuickEditLinks() { + $this->container->get('module_installer')->install(['quickedit']); + $this->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), ['access in-place editing']); + $quick_edit_selector = '#quickedit-entity-toolbar'; + $node_selector = '[data-quickedit-entity-id="node/1"]'; + $body_selector = '[data-quickedit-field-id="node/1/body/en/full"]'; + $web_assert = $this->assertSession(); + // Create a Content type and two test nodes. + $this->createContentType(['type' => 'page']); + $auth_role = Role::load(Role::AUTHENTICATED_ID); + $this->grantPermissions($auth_role, [ + 'edit any page content', + 'access content', + ]); + $node = $this->createNode( + [ + 'title' => 'Page One', + 'type' => 'page', + 'body' => [ + [ + 'value' => 'Regular NODE body for the test.', + 'format' => 'plain_text', + ], + ], + ] + ); + $page = $this->getSession()->getPage(); + $block_plugin = 'system_powered_by_block'; + + foreach ($this->getTestThemes() as $theme) { + + $this->enableTheme($theme); + + $block = $this->placeBlock($block_plugin); + $block_selector = $this->getBlockSelector($block); + // Load the same page twice. + foreach ([1, 2] as $page_load_times) { + $this->drupalGet('node/' . $node->id()); + // The 2nd page load we should already be in edit mode. + if ($page_load_times == 1) { + $this->enableEditMode(); + } + // In Edit mode clicking field should open QuickEdit toolbar. + $page->find('css', $body_selector)->click(); + $this->assertElementVisibleAfterWait('css', $quick_edit_selector); + + $this->disableEditMode(); + // Exiting Edit mode should close QuickEdit toolbar. + $web_assert->elementNotExists('css', $quick_edit_selector); + // When not in Edit mode QuickEdit toolbar should not open. + $page->find('css', $body_selector)->click(); + $web_assert->elementNotExists('css', $quick_edit_selector); + $this->enableEditMode(); + $this->openBlockForm($block_selector); + $page->find('css', $body_selector)->click(); + $this->assertElementVisibleAfterWait('css', $quick_edit_selector); + // Off-canvas dialog should be closed when opening QuickEdit toolbar. + $this->waitForOffCanvasToClose(); + + $this->openBlockForm($block_selector); + // QuickEdit toolbar should be closed when opening Off-canvas dialog. + $web_assert->elementNotExists('css', $quick_edit_selector); + } + // Check using contextual links to invoke QuickEdit and open the tray. + $this->drupalGet('node/' . $node->id()); + $web_assert->assertWaitOnAjaxRequest(); + $this->disableEditMode(); + // Open QuickEdit toolbar before going into Edit mode. + $this->clickContextualLink($node_selector, "Quick edit"); + $this->assertElementVisibleAfterWait('css', $quick_edit_selector); + // Open off-canvas and enter Edit mode via contextual link. + $this->clickContextualLink($block_selector, "Quick edit"); + $this->waitForOffCanvasToOpen(); + // QuickEdit toolbar should be closed when opening off-canvas dialog. + $web_assert->elementNotExists('css', $quick_edit_selector); + // Open QuickEdit toolbar via contextual link while in Edit mode. + $this->clickContextualLink($node_selector, "Quick edit", FALSE); + $this->waitForOffCanvasToClose(); + $this->assertElementVisibleAfterWait('css', $quick_edit_selector); + $this->disableEditMode(); + } + } + + /** + * Tests enabling and disabling Edit Mode. + */ + public function testEditModeEnableDisable() { + foreach ($this->getTestThemes() as $theme) { + $this->enableTheme($theme); + $block = $this->placeBlock('system_powered_by_block'); + foreach (['contextual_link', 'toolbar_link'] as $enable_option) { + $this->drupalGet('user'); + $this->assertEditModeDisabled(); + switch ($enable_option) { + // Enable Edit mode. + case 'contextual_link': + $this->clickContextualLink($this->getBlockSelector($block), "Quick edit"); + $this->waitForOffCanvasToOpen(); + $this->assertEditModeEnabled(); + break; + + case 'toolbar_link': + $this->enableEditMode(); + break; + } + $this->disableEditMode(); + + // Make another page request to ensure Edit mode is still disabled. + $this->drupalGet('user'); + $this->assertEditModeDisabled(); + // Make sure on this page request it also re-enables and disables + // correctly. + $this->enableEditMode(); + $this->disableEditMode(); + } + } + } + + /** + * Assert that edit mode has been properly enabled. + */ + protected function assertEditModeEnabled() { + $web_assert = $this->assertSession(); + // No contextual triggers should be hidden. + $web_assert->elementNotExists('css', '.contextual .trigger.visually-hidden'); + // The toolbar edit button should read "Editing". + $web_assert->elementContains('css', static::TOOLBAR_EDIT_LINK_SELECTOR, 'Editing'); + // The main canvas element should have the "js-settings-tray-edit-mode" class. + $web_assert->elementExists('css', '.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode'); + } + + /** + * Assert that edit mode has been properly disabled. + */ + protected function assertEditModeDisabled() { + $web_assert = $this->assertSession(); + // Contextual triggers should be hidden. + $web_assert->elementExists('css', '.contextual .trigger.visually-hidden'); + // No contextual triggers should be not hidden. + $web_assert->elementNotExists('css', '.contextual .trigger:not(.visually-hidden)'); + // The toolbar edit button should read "Edit". + $web_assert->elementContains('css', static::TOOLBAR_EDIT_LINK_SELECTOR, 'Edit'); + // The main canvas element should NOT have the "js-settings-tray-edit-mode" + // class. + $web_assert->elementNotExists('css', '.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode'); + } + + /** + * Press the toolbar Edit button provided by the contextual module. + */ + protected function pressToolbarEditButton() { + $this->assertSession()->waitForElement('css', '[data-contextual-id] .contextual-links a'); + $edit_button = $this->getSession() + ->getPage() + ->find('css', static::TOOLBAR_EDIT_LINK_SELECTOR); + $edit_button->press(); + } + + /** + * Creates a custom block. + * + * @param bool|string $title + * (optional) Title of block. When no value is given uses a random name. + * Defaults to FALSE. + * @param string $bundle + * (optional) Bundle name. Defaults to 'basic'. + * @param bool $save + * (optional) Whether to save the block. Defaults to TRUE. + * + * @return \Drupal\block_content\Entity\BlockContent + * Created custom block. + */ + protected function createBlockContent($title = FALSE, $bundle = 'basic', $save = TRUE) { + $title = $title ?: $this->randomName(); + $block_content = BlockContent::create([ + 'info' => $title, + 'type' => $bundle, + 'langcode' => 'en', + 'body' => [ + 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', + 'format' => 'plain_text', + ], + ]); + if ($block_content && $save === TRUE) { + $block_content->save(); + } + return $block_content; + } + + /** + * Creates a custom block type (bundle). + * + * @param string $label + * The block type label. + * @param bool $create_body + * Whether or not to create the body field. + * + * @return \Drupal\block_content\Entity\BlockContentType + * Created custom block type. + */ + protected function createBlockContentType($label, $create_body = FALSE) { + $bundle = BlockContentType::create([ + 'id' => $label, + 'label' => $label, + 'revision' => FALSE, + ]); + $bundle->save(); + if ($create_body) { + block_content_add_body_field($bundle->id()); + } + return $bundle; + } + + /** + * Tests that contextual links in custom blocks are changed. + * + * "Quick edit" is quickedit.module link. + * "Quick edit settings" is settings_tray.module link. + */ + public function testCustomBlockLinks() { + $this->container->get('module_installer')->install(['quickedit']); + $this->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), ['access in-place editing']); + $this->drupalGet('user'); + $page = $this->getSession()->getPage(); + $links = $page->findAll('css', "#block-custom .contextual-links li a"); + $link_labels = []; + /** @var \Behat\Mink\Element\NodeElement $link */ + foreach ($links as $link) { + $link_labels[$link->getAttribute('href')] = $link->getText(); + } + $href = array_search('Quick edit', $link_labels); + $this->assertEquals('', $href); + $href = array_search('Quick edit settings', $link_labels); + $this->assertTrue(strstr($href, '/admin/structure/block/manage/custom/off-canvas?destination=user/2') !== FALSE); + } + + /** + * Gets the block CSS selector. + * + * @param \Drupal\block\Entity\Block $block + * The block. + * + * @return string + * The CSS selector. + */ + public function getBlockSelector(Block $block) { + return '#block-' . str_replace('_', '-', $block->id()); + } + + /** + * Determines if the label input is visible. + * + * @return bool + * TRUE if the label is visible, FALSE if it is not. + */ + protected function isLabelInputVisible() { + return $this->getSession()->getPage()->find('css', static::LABEL_INPUT_SELECTOR)->isVisible(); + } + + /** + * Test that validation errors appear in the off-canvas dialog. + */ + public function testValidationMessages() { + $page = $this->getSession()->getPage(); + $web_assert = $this->assertSession(); + foreach ($this->getTestThemes() as $theme) { + $this->enableTheme($theme); + $block = $this->placeBlock('settings_tray_test_validation'); + $this->drupalGet('user'); + $this->enableEditMode(); + $this->openBlockForm($this->getBlockSelector($block)); + $page->pressButton('Save Block with validation error'); + $web_assert->assertWaitOnAjaxRequest(); + // The settings_tray_test_validation test plugin form always has a + // validation error. + $web_assert->elementContains('css', '#drupal-off-canvas', 'Sorry system error. Please save again'); + $this->disableEditMode(); + $block->delete(); + } + } + + /** + * {@inheritdoc} + */ + protected function getTestThemes() { + // Remove 'seven' theme. Setting Tray "Edit Mode" will not work with 'seven' + // because it removes all contextual links the off-canvas dialog should. + return array_filter(parent::getTestThemes(), function ($theme) { + return $theme !== 'seven'; + }); + } + + /** + * Tests that blocks with configuration overrides are disabled. + */ + public function testOverriddenBlock() { + $web_assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + $overridden_block = $this->placeBlock('system_powered_by_block', [ + 'id' => 'overridden_block', + 'label_display' => 1, + 'label' => 'This will be overridden.', + ]); + $this->drupalGet('user'); + $block_selector = $this->getBlockSelector($overridden_block); + // Confirm the block is marked as Settings Tray editable. + $this->assertEquals('editable', $page->find('css', $block_selector)->getAttribute('data-drupal-settingstray')); + // Confirm the label is not overridden. + $web_assert->elementContains('css', $block_selector, 'This will be overridden.'); + $this->enableEditMode(); + $this->openBlockForm($block_selector); + + // Confirm the block Settings Tray functionality is disabled when block is + // overridden. + $this->container->get('state')->set('settings_tray_override_test.block', TRUE); + $overridden_block->save(); + $block_config = \Drupal::configFactory()->getEditable('block.block.overridden_block'); + $block_config->set('settings', $block_config->get('settings'))->save(); + + $this->drupalGet('user'); + $this->assertOverriddenBlockDisabled($overridden_block, 'Now this will be the label.'); + + // Test a non-overridden block does show the form in the off-canvas dialog. + $block = $this->placeBlock('system_powered_by_block', [ + 'label_display' => 1, + 'label' => 'Labely label', + ]); + $this->drupalGet('user'); + $block_selector = $this->getBlockSelector($block); + // Confirm the block is marked as Settings Tray editable. + $this->assertEquals('editable', $page->find('css', $block_selector)->getAttribute('data-drupal-settingstray')); + // Confirm the label is not overridden. + $web_assert->elementContains('css', $block_selector, 'Labely label'); + $this->openBlockForm($block_selector); + } + + /** + * Test blocks with overridden related configuration removed when overridden. + */ + public function testOverriddenConfigurationRemoved() { + $web_assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Confirm the branding block does include 'site_information' section when + // the site name is not overridden. + $branding_block = $this->placeBlock('system_branding_block'); + $this->drupalGet('user'); + $this->enableEditMode(); + $this->openBlockForm($this->getBlockSelector($branding_block)); + $web_assert->fieldExists('settings[site_information][site_name]'); + // Confirm the branding block does not include 'site_information' section + // when the site name is overridden. + $this->container->get('state')->set('settings_tray_override_test.site_name', TRUE); + $this->drupalGet('user'); + $this->openBlockForm($this->getBlockSelector($branding_block)); + $web_assert->fieldNotExists('settings[site_information][site_name]'); + $page->pressButton('Save Site branding'); + $this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)'); + $web_assert->assertWaitOnAjaxRequest(); + // Confirm we did not save changes to the configuration. + $this->assertEquals('Llama Fan Club', \Drupal::configFactory()->get('system.site')->get('name')); + $this->assertEquals('Drupal', \Drupal::configFactory()->getEditable('system.site')->get('name')); + + // Add a link or the menu will not render. + $menu_link_content = MenuLinkContent::create([ + 'title' => 'This is on the menu', + 'menu_name' => 'main', + 'link' => ['uri' => 'route:'], + ]); + $menu_link_content->save(); + // Confirm the menu block does include menu section when the menu is not + // overridden. + $menu_block = $this->placeBlock('system_menu_block:main'); + $web_assert->assertWaitOnAjaxRequest(); + $this->drupalGet('user'); + $web_assert->pageTextContains('This is on the menu'); + $this->openBlockForm($this->getBlockSelector($menu_block)); + $web_assert->elementExists('css', '#menu-overview'); + + // Confirm the menu block does not include menu section when the menu is + // overridden. + $this->container->get('state')->set('settings_tray_override_test.menu', TRUE); + $this->drupalGet('user'); + $web_assert->pageTextContains('This is on the menu'); + $menu_with_overrides = \Drupal::configFactory()->get('system.menu.main')->get(); + $menu_without_overrides = \Drupal::configFactory()->getEditable('system.menu.main')->get(); + $this->openBlockForm($this->getBlockSelector($menu_block)); + $web_assert->elementNotExists('css', '#menu-overview'); + $page->pressButton('Save Main navigation'); + $this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)'); + $web_assert->assertWaitOnAjaxRequest(); + // Confirm we did not save changes to the configuration. + $this->assertEquals('Labely label', \Drupal::configFactory()->get('system.menu.main')->get('label')); + $this->assertEquals('Main navigation', \Drupal::configFactory()->getEditable('system.menu.main')->get('label')); + $this->assertEquals($menu_with_overrides, \Drupal::configFactory()->get('system.menu.main')->get()); + $this->assertEquals($menu_without_overrides, \Drupal::configFactory()->getEditable('system.menu.main')->get()); + $web_assert->pageTextContains('This is on the menu'); + } + /** + * Asserts that an overridden block has Settings Tray disabled. + * + * @param \Drupal\block\Entity\Block $overridden_block + * The overridden block. + * @param string $override_text + * The override text that should appear in the block. + */ + protected function assertOverriddenBlockDisabled(Block $overridden_block, $override_text) { + $web_assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + $block_selector = $this->getBlockSelector($overridden_block); + $block_id = $overridden_block->id(); + // Confirm the block does not have a quick edit link. + $contextual_links = $page->findAll('css', "$block_selector .contextual-links li a"); + $this->assertNotEmpty($contextual_links); + foreach ($contextual_links as $link) { + $this->assertNotContains("/admin/structure/block/manage/$block_id/off-canvas", $link->getAttribute('href')); + } + // Confirm the block is not marked as Settings Tray editable. + $this->assertFalse($page->find('css', $block_selector) + ->hasAttribute('data-drupal-settingstray')); + + // Confirm the text is actually overridden. + $web_assert->elementContains('css', $this->getBlockSelector($overridden_block), $override_text); + } + +}