diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index 5c5fa46278..f6bbe7873d 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -364,6 +364,7 @@ druplicon
drush
drépal
détruire
+eacute
editables
editunblock
eerste
@@ -428,6 +429,7 @@ fielditem
fieldlayout
fieldlinks
fieldnames
+fieldnid
fieldsets
filelist
filemime
diff --git a/core/modules/layout_builder/css/layout-builder.css b/core/modules/layout_builder/css/layout-builder.css
index 50bd2005b1..53821ee112 100644
--- a/core/modules/layout_builder/css/layout-builder.css
+++ b/core/modules/layout_builder/css/layout-builder.css
@@ -178,3 +178,7 @@
.layout-builder-components-table .tabledrag-changed-warning {
display: none !important;
}
+
+#drupal-off-canvas .configured-conditions {
+ margin: 0.5em 0 1.5em;
+}
diff --git a/core/modules/layout_builder/layout_builder.links.contextual.yml b/core/modules/layout_builder/layout_builder.links.contextual.yml
index 4bbfcc9e64..60c3da0f28 100644
--- a/core/modules/layout_builder/layout_builder.links.contextual.yml
+++ b/core/modules/layout_builder/layout_builder.links.contextual.yml
@@ -27,3 +27,13 @@ layout_builder_block_remove:
class: ['use-ajax']
data-dialog-type: dialog
data-dialog-renderer: off_canvas
+
+layout_builder_block_visibility:
+ title: 'Control visibility'
+ route_name: 'layout_builder.visibility'
+ group: 'layout_builder_block'
+ options:
+ attributes:
+ class: ['use-ajax']
+ data-dialog-type: dialog
+ data-dialog-renderer: off_canvas
diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml
index 1e5fd50ff1..6559ce2f4d 100644
--- a/core/modules/layout_builder/layout_builder.routing.yml
+++ b/core/modules/layout_builder/layout_builder.routing.yml
@@ -145,3 +145,42 @@ layout_builder.move_block:
parameters:
section_storage:
layout_builder_tempstore: TRUE
+
+layout_builder.visibility:
+ path: '/layout_builder/visibility/block/{section_storage_type}/{section_storage}/{delta}/{uuid}'
+ defaults:
+ _form: '\Drupal\layout_builder\Form\BlockVisibilityForm'
+ _title_callback: '\Drupal\layout_builder\Form\BlockVisibilityForm::title'
+ requirements:
+ _layout_builder_access: 'view'
+ options:
+ _admin_route: TRUE
+ parameters:
+ section_storage:
+ layout_builder_tempstore: TRUE
+
+layout_builder.add_visibility:
+ path: '/layout_builder/visibility/block/{section_storage_type}/{section_storage}/{delta}/{uuid}/{plugin_id}'
+ defaults:
+ _form: '\Drupal\layout_builder\Form\ConfigureVisibilityForm'
+ _title_callback: '\Drupal\layout_builder\Form\ConfigureVisibilityForm::title'
+ requirements:
+ _layout_builder_access: 'view'
+ options:
+ _admin_route: TRUE
+ parameters:
+ section_storage:
+ layout_builder_tempstore: TRUE
+
+layout_builder.delete_visibility:
+ path: '/layout_builder/visibility/block/{section_storage_type}/{section_storage}/{delta}/{uuid}/{plugin_id}/delete'
+ defaults:
+ _form: '\Drupal\layout_builder\Form\DeleteVisibilityForm'
+ _title_callback: '\Drupal\layout_builder\Form\DeleteVisibilityForm::title'
+ requirements:
+ _layout_builder_access: 'view'
+ options:
+ _admin_route: TRUE
+ parameters:
+ section_storage:
+ layout_builder_tempstore: TRUE
diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml
index 4e9fc3acda..cd248f3969 100644
--- a/core/modules/layout_builder/layout_builder.services.yml
+++ b/core/modules/layout_builder/layout_builder.services.yml
@@ -49,6 +49,11 @@ services:
inline_block.usage:
class: Drupal\layout_builder\InlineBlockUsage
arguments: ['@database']
+ layout_builder.component_visibility_subscriber:
+ class: Drupal\layout_builder\EventSubscriber\SectionComponentVisibility
+ arguments: ['@context.handler', '@plugin.manager.condition']
+ tags:
+ - { name: event_subscriber }
layout_builder.controller.entity_form:
# Override the entity form controller to handle the entity layout_builder
# operation.
diff --git a/core/modules/layout_builder/src/EventSubscriber/SectionComponentVisibility.php b/core/modules/layout_builder/src/EventSubscriber/SectionComponentVisibility.php
new file mode 100644
index 0000000000..9d844b8b87
--- /dev/null
+++ b/core/modules/layout_builder/src/EventSubscriber/SectionComponentVisibility.php
@@ -0,0 +1,88 @@
+contextHandler = $context_handler;
+ $this->conditionManager = $condition_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ // Run before BlockComponentRenderArray (priority 100), so that we can
+ // stop propagation and prevent rendering the component.
+ $events[LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY] = ['onBuildRender', 255];
+ return $events;
+ }
+
+ /**
+ * Determines the visibility of section component.
+ *
+ * @param \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent $event
+ * The section component build render array event.
+ */
+ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
+ if ($event->inPreview()) {
+ return;
+ }
+
+ $conditions = [];
+
+ $visibility = $event->getComponent()->get('visibility') ?: [];
+ foreach ($visibility as $uuid => $configuration) {
+ $condition = $this->conditionManager->createInstance($configuration['id'], $configuration);
+ if ($condition instanceof ContextAwarePluginInterface) {
+ $this->contextHandler->applyContextMapping($condition, $event->getContexts());
+ }
+ $event->addCacheableDependency($condition);
+ $conditions[$uuid] = $condition;
+ }
+
+ $visibility_operator = $event->getComponent()->get('visibility_operator') ?: 'and';
+
+ if ($conditions && !$this->resolveConditions($conditions, $visibility_operator)) {
+ // If conditions do not resolve, do not process other subscribers.
+ $event->stopPropagation();
+ }
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Form/BlockVisibilityForm.php b/core/modules/layout_builder/src/Form/BlockVisibilityForm.php
new file mode 100644
index 0000000000..a8e9af52a6
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/BlockVisibilityForm.php
@@ -0,0 +1,357 @@
+conditionManager = $condition_manager;
+ $this->formBuilder = $form_builder;
+ $this->layoutTempstoreRepository = $layout_tempstore_repository;
+ $this->contextRepository = $context_repository;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.condition'),
+ $container->get('form_builder'),
+ $container->get('layout_builder.tempstore_repository'),
+ $container->get('context.repository')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'layout_builder_block_visibility';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $uuid = NULL) {
+ $this->sectionStorage = $section_storage;
+ $this->delta = $delta;
+ $this->uuid = $uuid;
+
+ // Any visibility conditions that have already been added to the block.
+ $visibility_conditions_applied_to_block = $this->getCurrentComponent()->get('visibility') ?: [];
+
+ // Visibility condition types that can be added to a block.
+ $conditions_available_to_block = [];
+ foreach ($this->conditionManager->getFilteredDefinitions('layout_builder', $this->contextRepository->getAvailableContexts($section_storage)) as $plugin_id => $definition) {
+ $conditions_available_to_block[$plugin_id] = $definition['label'];
+ }
+
+ $items = [];
+ foreach ($visibility_conditions_applied_to_block as $visibility_id => $configuration) {
+ /** @var \Drupal\Core\Condition\ConditionInterface $condition */
+ $condition = $this->conditionManager->createInstance($configuration['id'], $configuration);
+ $options = [
+ 'attributes' => [
+ 'class' => ['use-ajax'],
+ 'data-dialog-type' => 'dialog',
+ 'data-dialog-renderer' => 'off_canvas',
+ 'data-outside-in-edit' => TRUE,
+ ],
+ ];
+ $items[$visibility_id] = [
+ 'label' => $this->t('@condition_name: @condition_summary', [
+ '@condition_name' => $condition->getPluginDefinition()['label'],
+ '@condition_summary' => $condition->summary(),
+ ]),
+ 'edit' => [
+ 'data' => [
+ '#type' => 'link',
+ '#title' => $this->t('Edit'),
+ '#url' => Url::fromRoute('layout_builder.add_visibility', $this->getParameters($visibility_id), $options),
+ ],
+ ],
+ 'delete' => [
+ 'data' => [
+ '#type' => 'link',
+ '#title' => $this->t('Delete'),
+ '#url' => Url::fromRoute('layout_builder.delete_visibility', $this->getParameters($visibility_id), $options),
+ ],
+ ],
+ ];
+ }
+
+ if ($items) {
+ $form['visibility'] = [
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#theme' => 'table',
+ '#rows' => $items,
+ '#caption' => $this->t('Configured Conditions'),
+ '#weight' => 10,
+ ];
+ }
+
+ $form['condition'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Add a visibility condition'),
+ '#options' => $conditions_available_to_block,
+ '#empty_value' => '',
+ '#weight' => 20,
+ ];
+
+ // Determines if multiple conditions should be applied with 'and' or 'or'.
+ $form['operator'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Operator'),
+ '#options' => [
+ 'and' => $this->t('And'),
+ 'or' => $this->t('Or'),
+ ],
+ '#default_value' => $this->getCurrentComponent()->get('visibility_operator') ?: 'and',
+ // This field is not necessary until multiple conditions are added.
+ '#access' => count($items) > 0,
+ // If there are two or more visibility conditions, this field appears
+ // above the list of existing conditions (weight 10).
+ // If there is only one visibility condition, and a second one is being
+ // added, then this field appears between the 'Add a visibility condition'
+ // dropdown (weight 20) and the submit button (weight 40).
+ '#weight' => count($items) === 1 ? 30 : 0,
+ ];
+
+ // This is a submit button that only appears once two or more visibility
+ // conditions are present. This submit button appears so the user can
+ // update the visibility operator, a setting that impacts the entire block.
+ // This is different than the default submit button/handler for this form,
+ // which is used to add a visibility condition to the block.
+ $form['update_operator'] = [
+ '#type' => 'submit',
+ '#access' => count($items) > 1,
+ '#weight' => 5,
+ '#value' => $this->t('Update operator'),
+ '#submit' => ['::updateOperator'],
+ ];
+
+ if (count($items) === 1) {
+ // If there is only one visibility condition, hide the operator field
+ // until a second condition is selected to be added to the block.
+ $form['operator']['#states'] = [
+ 'invisible' => [
+ '[name="condition"]' => ['value' => ''],
+ ],
+ ];
+ }
+
+ $form['actions']['#weight'] = 40;
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Add condition'),
+ // Submit button is only visible if a condition is selected.
+ '#states' => [
+ 'invisible' => [
+ '[name="condition"]' => ['value' => ''],
+ ],
+ ],
+ ];
+
+ $form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($this->uuid);
+
+ if ($this->isAjax()) {
+ $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+ $form['update_operator']['#ajax']['callback'] = '::ajaxSubmit';
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ // If the submit was triggered by the "update operator" button, just
+ // rebuild the layout UI and close the dialog.
+ if (isset($triggering_element['#submit'][0]) && ($triggering_element['#submit'][0] === '::updateOperator')) {
+ return $this->rebuildAndClose($this->sectionStorage);
+ }
+
+ // Adding a visibility condition to a block is a two step process. This
+ // submit handler is triggered after completion of step 1: choosing the
+ // condition to add. The logic below opens a configuration form for step 2:
+ // configuring the condition that was just added.
+ $condition = $form_state->getValue('condition');
+ $parameters = $this->getParameters($condition);
+
+ // Build the configuration form to be used in step 2.
+ $new_form = $this->formBuilder->getForm(ConfigureVisibilityForm::class, $this->sectionStorage, $parameters['delta'], $parameters['uuid'], $parameters['plugin_id']);
+
+ // @todo The changes to #action/actions need to be documented or refactored
+ // to better resemble other dual-dialog forms in Layout Builder.
+ $new_form['#action'] = (new Url('layout_builder.add_visibility', $parameters))->toString();
+ $url = new Url('layout_builder.add_visibility', $parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE, '_wrapper_format' => 'drupal_ajax']]);
+ $new_form['actions']['submit']['#attached']['drupalSettings']['ajax'][$new_form['actions']['submit']['#id']]['url'] = $url->toString();
+ $response = new AjaxResponse();
+ $response->addCommand(new OpenOffCanvasDialogCommand($this->t('Configure condition'), $new_form));
+ return $response;
+ }
+
+ /**
+ * Submit handler for updating just the visibility operator.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state object.
+ */
+ public function updateOperator(array $form, FormStateInterface $form_state) {
+ $operator_value = $form_state->getValue('operator');
+ $component = $this->getCurrentComponent();
+ $component->set('visibility_operator', $operator_value);
+ $this->layoutTempstoreRepository->set($this->sectionStorage);
+ $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $parameters = $this->getParameters($form_state->getValue('condition'));
+ $operator = $form_state->getValue('operator');
+ $parameters['operator'] = $operator === 'or' ? $operator : 'and';
+ $url = new Url('layout_builder.add_visibility', $parameters);
+ $form_state->setRedirectUrl($url);
+ }
+
+ /**
+ * Gets the parameters needed for the various Url() and form invocations.
+ *
+ * @param string $visibility_id
+ * The ID of the visibility plugin.
+ *
+ * @return array
+ * List of URL parameters.
+ */
+ protected function getParameters($visibility_id) {
+ return [
+ 'section_storage_type' => $this->sectionStorage->getStorageType(),
+ 'section_storage' => $this->sectionStorage->getStorageId(),
+ 'delta' => $this->delta,
+ 'uuid' => $this->uuid,
+ 'plugin_id' => $visibility_id,
+ ];
+ }
+
+ /**
+ * Provides a title callback.
+ *
+ * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
+ * The section storage.
+ * @param int $delta
+ * The original delta of the section.
+ * @param string $uuid
+ * The UUID of the block being updated.
+ *
+ * @return string
+ * The title for the block visibility form.
+ */
+ public function title(SectionStorageInterface $section_storage, $delta, $uuid) {
+ $block_label = $section_storage
+ ->getSection($delta)
+ ->getComponent($uuid)
+ ->getPlugin()
+ ->label();
+
+ return $this->t('Configure visibility rules for the @block_label block', ['@block_label' => $block_label]);
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Form/ConfigureVisibilityForm.php b/core/modules/layout_builder/src/Form/ConfigureVisibilityForm.php
new file mode 100644
index 0000000000..62bba9ea0f
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/ConfigureVisibilityForm.php
@@ -0,0 +1,330 @@
+layoutTempstoreRepository = $layout_tempstore_repository;
+ $this->conditionManager = $condition_manager;
+ $this->uuidGenerator = $uuid_generator;
+ $this->pluginFormFactory = $plugin_form_manager;
+ $this->classResolver = $class_resolver;
+ $this->contextRepository = $context_repository;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('layout_builder.tempstore_repository'),
+ $container->get('plugin.manager.condition'),
+ $container->get('uuid'),
+ $container->get('plugin_form.factory'),
+ $container->get('class_resolver'),
+ $container->get('context.repository')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'layout_builder_configure_visibility';
+ }
+
+ /**
+ * Prepares the condition plugin based on the condition ID.
+ *
+ * @param string $condition_id
+ * A condition UUID, or the plugin ID used to create a new condition.
+ * @param array $value
+ * The condition configuration.
+ *
+ * @return \Drupal\Core\Condition\ConditionInterface
+ * The condition plugin.
+ */
+ protected function prepareCondition($condition_id, array $value) {
+ if ($value) {
+ return $this->conditionManager->createInstance($value['id'], $value);
+ }
+ /** @var \Drupal\Core\Condition\ConditionInterface $condition */
+ $condition = $this->conditionManager->createInstance($condition_id);
+ $configuration = $condition->getConfiguration();
+ $configuration['uuid'] = $this->uuidGenerator->generate();
+ $condition->setConfiguration($configuration);
+ return $condition;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $uuid = NULL, $plugin_id = NULL) {
+ $this->sectionStorage = $section_storage;
+ $this->delta = $delta;
+ $this->uuid = $uuid;
+
+ $visibility_conditions = $this->getCurrentComponent()->get('visibility');
+ $configuration = !empty($visibility_conditions[$plugin_id]) ? $visibility_conditions[$plugin_id] : [];
+ $this->configuration = $configuration;
+ $this->condition = $this->prepareCondition($plugin_id, $configuration);
+
+ $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts($section_storage));
+
+ $form['#tree'] = TRUE;
+ $form['settings'] = [];
+ $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+ $form['settings'] = $this->getConditionPluginForm($this->condition)->buildConfigurationForm($form['settings'], $subform_state);
+ $form['settings']['id'] = [
+ '#type' => 'value',
+ '#value' => $this->condition->getPluginId(),
+ ];
+
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $configuration ? $this->t('Update') : $this->t('Add condition'),
+ '#button_type' => 'primary',
+ ];
+
+ // If one is not already present, add a hidden field with the value of
+ // the operator field from BlockVisibilityForm - the form that precedes
+ // this one when adding/updating a visibility condition.
+ if (!$form_state->getValue('operator')) {
+ // Get the input values from the form that preceded this one.
+ $user_input = $form_state->getUserInput();
+ $form['operator']['#type'] = 'hidden';
+ if (isset($user_input['operator']) && $user_input['operator'] === 'or') {
+ $form['operator']['#value'] = $user_input['operator'];
+ }
+ else {
+ $form['operator']['#value'] = 'and';
+ }
+ }
+
+ $form['back_button'] = [
+ '#type' => 'link',
+ '#url' => Url::fromRoute('layout_builder.visibility',
+ [
+ 'section_storage_type' => $section_storage->getStorageType(),
+ 'section_storage' => $section_storage->getStorageId(),
+ 'delta' => $delta,
+ 'uuid' => $uuid,
+ ]
+ ),
+ '#title' => $this->t('Back'),
+ ];
+
+ $form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($this->uuid);
+
+ if ($this->isAjax()) {
+ $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+ $form['back_button']['#attributes'] = [
+ 'class' => ['use-ajax'],
+ 'data-dialog-type' => 'dialog',
+ 'data-dialog-renderer' => 'off_canvas',
+ ];
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+ $this->getConditionPluginForm($this->condition)->validateConfigurationForm($form['settings'], $subform_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Call the plugin submit handler.
+ $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+ $this->getConditionPluginForm($this->condition)->submitConfigurationForm($form, $subform_state);
+
+ // If this block is context-aware, set the context mapping.
+ if ($this->condition instanceof ContextAwarePluginInterface) {
+ $this->condition->setContextMapping($subform_state->getValue('context_mapping', []));
+ }
+
+ $configuration = $this->condition->getConfiguration();
+
+ $component = $this->getCurrentComponent();
+ $visibility_conditions = $component->get('visibility');
+ $visibility_conditions[$configuration['uuid']] = $configuration;
+ $component->set('visibility', $visibility_conditions);
+ $component->set('visibility_operator', $form_state->getValue('operator'));
+
+ $this->layoutTempstoreRepository->set($this->sectionStorage);
+ $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
+ return $this->rebuildAndClose($this->sectionStorage);
+ }
+
+ /**
+ * Retrieves the plugin form for a given condition.
+ *
+ * @param \Drupal\Core\Condition\ConditionInterface $condition
+ * The condition plugin.
+ *
+ * @return \Drupal\Core\Plugin\PluginFormInterface
+ * The plugin form for the condition.
+ */
+ protected function getConditionPluginForm(ConditionInterface $condition) {
+ if ($condition instanceof PluginWithFormsInterface) {
+ return $this->pluginFormFactory->createInstance($condition, 'configure');
+ }
+ return $condition;
+ }
+
+ /**
+ * Provides a title callback.
+ *
+ * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
+ * The section storage.
+ * @param int $delta
+ * The original delta of the section.
+ * @param string $uuid
+ * The UUID of the block being updated.
+ *
+ * @return string
+ * The title for the block visibility form.
+ */
+ public function title(SectionStorageInterface $section_storage, $delta, $uuid) {
+ $block_label = $section_storage
+ ->getSection($delta)
+ ->getComponent($uuid)
+ ->getPlugin()
+ ->label();
+
+ return $this->t('Configure visibility rule for the @block_label block', ['@block_label' => $block_label]);
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Form/DeleteVisibilityForm.php b/core/modules/layout_builder/src/Form/DeleteVisibilityForm.php
new file mode 100644
index 0000000000..c6c1ee1b1c
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/DeleteVisibilityForm.php
@@ -0,0 +1,205 @@
+layoutTempstoreRepository = $layout_tempstore_repository;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('layout_builder.tempstore_repository')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $uuid = NULL, $plugin_id = NULL) {
+ $this->sectionStorage = $section_storage;
+ $this->delta = $delta;
+ $this->uuid = $uuid;
+ $this->pluginId = $plugin_id;
+ $form = parent::buildForm($form, $form_state);
+ $form['actions']['cancel'] = $this->buildCancelLink();
+ $form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($this->uuid);
+ if ($this->isAjax()) {
+ $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return $this->t('Are you sure you want to delete this visibility condition?');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ $parameters = $this->getParameters();
+ return new Url('layout_builder.visibility', $parameters);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'layout_builder_delete_visibility';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $component = $this->getCurrentComponent();
+ $visibility_conditions = $component->get('visibility');
+ unset($visibility_conditions[$this->pluginId]);
+ $component->set('visibility', $visibility_conditions);
+ $this->layoutTempstoreRepository->set($this->sectionStorage);
+ $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
+ }
+
+ /**
+ * Build a cancel button for the confirm form.
+ */
+ protected function buildCancelLink() {
+ return [
+ '#type' => 'button',
+ '#value' => $this->getCancelText(),
+ '#ajax' => [
+ 'callback' => '::ajaxCancel',
+ ],
+ ];
+ }
+
+ /**
+ * Provides an ajax callback for the cancel button.
+ */
+ public function ajaxCancel(array &$form, FormStateInterface $form_state) {
+ $parameters = $this->getParameters();
+ $new_form = \Drupal::formBuilder()->getForm(BlockVisibilityForm::class, $this->sectionStorage, $parameters['delta'], $parameters['uuid']);
+ $new_form['#action'] = $this->getCancelUrl()->toString();
+ $response = new AjaxResponse();
+ $response->addCommand(new OpenOffCanvasDialogCommand($this->t('Delete condition'), $new_form));
+ return $response;
+ }
+
+ /**
+ * Gets the parameters needed for the various Url() and form invocations.
+ *
+ * @return array
+ * List of Url parameters.
+ */
+ protected function getParameters() {
+ return [
+ 'section_storage_type' => $this->sectionStorage->getStorageType(),
+ 'section_storage' => $this->sectionStorage->getStorageId(),
+ 'delta' => $this->delta,
+ 'uuid' => $this->uuid,
+ ];
+ }
+
+ /**
+ * Provides a title callback.
+ *
+ * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
+ * The section storage.
+ * @param int $delta
+ * The original delta of the section.
+ * @param string $uuid
+ * The UUID of the block being updated.
+ *
+ * @return string
+ * The title for the block visibility form.
+ */
+ public function title(SectionStorageInterface $section_storage, $delta, $uuid) {
+ $block_label = $section_storage
+ ->getSection($delta)
+ ->getComponent($uuid)
+ ->getPlugin()
+ ->label();
+
+ return $this->t('Delete visibility rule for the @block_label block', ['@block_label' => $block_label]);
+ }
+
+ protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
+ return $this->rebuildAndClose($this->sectionStorage);
+ }
+
+}
diff --git a/core/modules/layout_builder/src/SectionComponentTrait.php b/core/modules/layout_builder/src/SectionComponentTrait.php
new file mode 100644
index 0000000000..06668c0b51
--- /dev/null
+++ b/core/modules/layout_builder/src/SectionComponentTrait.php
@@ -0,0 +1,51 @@
+sectionStorage->getSection($this->delta);
+ }
+
+ /**
+ * Retrieves the current component being edited by the form.
+ *
+ * @return \Drupal\layout_builder\SectionComponent
+ * The current section component.
+ */
+ public function getCurrentComponent() {
+ return $this->getCurrentSection()->getComponent($this->uuid);
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockVisibilityTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockVisibilityTest.php
new file mode 100644
index 0000000000..36992f342c
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/BlockVisibilityTest.php
@@ -0,0 +1,315 @@
+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->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
+ $this->submitForm(
+ ['layout[enabled]' => TRUE],
+ 'Save'
+ );
+
+ $this->createNode([
+ 'type' => 'bundle_with_section_field',
+ 'body' => [
+ [
+ 'value' => 'The node body',
+ ],
+ ],
+ ])->save();
+
+ $this->createNode([
+ 'type' => 'bundle_with_section_field',
+ 'body' => [
+ [
+ 'value' => 'The node body',
+ ],
+ ],
+ ])->save();
+ }
+
+ /**
+ * Tests conditional visibility.
+ */
+ public function testConditionalVisibility() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Remove all of the sections from the page.
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
+
+ $page->clickLink('Remove Section 1');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->pressButton('Remove');
+ $assert_session->assertWaitOnAjaxRequest();
+ // Assert that there are no sections on the page.
+ $assert_session->pageTextNotContains('Remove Section 1');
+ $assert_session->pageTextNotContains('Add block');
+
+ $page->clickLink('Add section');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
+
+ $page->clickLink('Three column');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.layout-builder-configure-section'));
+ $page->pressButton('Add section');
+
+ $blocks_in_layout = [
+ [
+ 'label' => 'ID',
+ 'region_selector' => '.layout__region--first',
+ 'rendered_block_selector' => '.block-field-blocknodebundle-with-section-fieldnid',
+ ],
+ [
+ 'label' => 'Powered by Drupal',
+ 'region_selector' => '.layout__region--second',
+ 'rendered_block_selector' => '.block-system-powered-by-block',
+ ],
+ [
+ 'label' => 'Body',
+ 'region_selector' => '.layout__region--third',
+ 'rendered_block_selector' => '.block-field-blocknodebundle-with-section-fieldbody',
+ ],
+ ];
+
+ foreach ($blocks_in_layout as $block) {
+ $rendered_block_selector = $block['rendered_block_selector'];
+ $this->addBlock($block['label'], $block['region_selector'], "#layout-builder $rendered_block_selector");
+ }
+
+ $page->pressButton('Save layout');
+
+ foreach (['node/1', 'node/2'] as $path) {
+ $this->drupalGet($path);
+ foreach ($blocks_in_layout as $block) {
+ $this->assertSession()->elementExists('css', $block['rendered_block_selector']);
+ }
+ $assert_session->pageTextContains('The node body');
+ }
+
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
+
+ // Confirm "Control visibility" contextual links available on each block.
+ foreach ($blocks_in_layout as $block) {
+ $rendered_block_selector = $block['rendered_block_selector'];
+ $assert_session->elementExists('css', "#layout-builder $rendered_block_selector .layout-builder-block-visibility a");
+ }
+
+ // Test Request Path visibility rule.
+ $this->beginAddCondition('request_path');
+ $page->checkField('settings[negate]');
+ $page->findField('settings[pages]')->setValue('/node/2');
+ $page->pressButton('Add condition');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->pressButton('Save layout');
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The node body');
+ $this->drupalGet('node/2');
+ $assert_session->pageTextNotContains('The node body');
+
+ // Confirm that editing an existing condition works.
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
+ $this->clickContextualLink(static::BODY_FIELDBLOCK_SELECTOR, 'Control visibility');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
+ $page->clickLink('Edit');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '[value="Update"]'));
+ $page->findField('settings[pages]')->setValue('/node/1');
+ $page->pressButton('Update');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->pressButton('Save layout');
+ $this->drupalGet('node/1');
+ $assert_session->pageTextNotContains('The node body');
+ $this->drupalGet('node/2');
+ $assert_session->pageTextContains('The node body');
+
+ // Confirm 'or' operator works ('and' is the default operator)
+ $this->beginAddCondition('request_path', 'or');
+ $page->checkField('settings[negate]');
+ $page->findField('settings[pages]')->setValue('/node/2');
+ $page->pressButton('Add condition');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->pressButton('Save layout');
+
+ // Test Current Theme visibility rule.
+ $this->removeVisibilityConditions();
+ $this->beginAddCondition('current_theme');
+ $page->checkField('settings[negate]');
+ $page->findField('settings[theme]')->setValue('classy');
+ $page->pressButton('Add condition');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->pressButton('Save layout');
+ $this->drupalGet('node/1');
+ $assert_session->pageTextNotContains('The node body');
+ $this->drupalGet('node/2');
+ $assert_session->pageTextNotContains('The node body');
+
+ // Test User Role visibility rule.
+ $this->removeVisibilityConditions();
+ $this->beginAddCondition('user_role');
+ $page->checkField('settings[negate]');
+ $page->checkField('settings[roles][anonymous]');
+ $page->pressButton('Add condition');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->pressButton('Save layout');
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The node body');
+ $this->drupalGet('node/2');
+ $assert_session->pageTextContains('The node body');
+
+ $this->drupalLogout();
+
+ $this->drupalGet('node/1');
+ $assert_session->pageTextNotContains('The node body');
+ $this->drupalGet('node/2');
+ $assert_session->pageTextNotContains('The node body');
+ }
+
+ /**
+ * Begins adding a visibility condition to the body field block.
+ *
+ * @param string $condition
+ * The visibility condition to add.
+ * @param string $operator
+ * The and/or operator when multiple conditions present.
+ */
+ protected function beginAddCondition($condition, $operator = '') {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
+ $this->clickContextualLink(static::BODY_FIELDBLOCK_SELECTOR, 'Control visibility');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
+ $page->findField('condition')->setValue($condition);
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '[value="Add condition"]'));
+ if (!empty($operator)) {
+ $page->findField('operator')->setValue($operator);
+ }
+ $page->pressButton('Add condition');
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '[name="settings[negate]"]'));
+ }
+
+ /**
+ * Removes the visibility rules from the body field block.
+ */
+ protected function removeVisibilityConditions() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
+ $this->clickContextualLink(static::BODY_FIELDBLOCK_SELECTOR, 'Control visibility');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
+ $page->clickLink('Delete');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '[value="Confirm"]'));
+ $page->pressButton('Confirm');
+ $assert_session->assertWaitOnAjaxRequest();
+
+ // If multiple conditions present, this method will need to run again.
+ $this->clickContextualLink(static::BODY_FIELDBLOCK_SELECTOR, 'Control visibility');
+ $assert_session->assertWaitOnAjaxRequest();
+ $close_button = $assert_session->waitForElementVisible('css', '[title="Close"]');
+ if ($page->hasLink('Delete')) {
+ $close_button->click();
+ $this->removeVisibilityConditions();
+ }
+ else {
+ $page->pressButton('Save layout');
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The node body');
+ $this->drupalGet('node/2');
+ $assert_session->pageTextContains('The node body');
+ }
+ }
+
+ /**
+ * Adds a block in the Layout Builder.
+ *
+ * @param string $block_link_text
+ * The link text to add the block.
+ * @param string $region_selector
+ * The link text to add the block.
+ * @param string $rendered_locator
+ * The CSS locator to confirm the block was rendered.
+ */
+ protected function addBlock($block_link_text, $region_selector, $rendered_locator) {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Add a new block.
+ $add_block_link_selector = "#layout-builder $region_selector a:contains('Add block')";
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', $add_block_link_selector));
+
+ $add_block_link = $page->find('css', $add_block_link_selector);
+ $add_block_link->click();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->clickLink($block_link_text);
+
+ // Wait for off-canvas dialog to reopen with block form.
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.layout-builder-add-block'));
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->pressButton('Add block');
+
+ // Wait for block form to be rendered in the Layout Builder.
+ $this->assertNotEmpty($assert_session->waitForElement('css', $rendered_locator));
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Unit/EventSubscriber/SectionComponentVisibilityTest.php b/core/modules/layout_builder/tests/src/Unit/EventSubscriber/SectionComponentVisibilityTest.php
new file mode 100644
index 0000000000..a503f1c9a7
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Unit/EventSubscriber/SectionComponentVisibilityTest.php
@@ -0,0 +1,229 @@
+conditionManager = new ConditionManager(
+ new \ArrayIterator([]),
+ new NullBackend('null'),
+ $this->prophesize(ModuleHandlerInterface::class)->reveal()
+ );
+ }
+
+ /**
+ * Ensure that nothing happens when previewing.
+ *
+ * @covers ::onBuildRender
+ */
+ public function testOnBuildRenderPreview() {
+ $subscriber = new SectionComponentVisibility(
+ new ContextHandler(),
+ $this->conditionManager
+ );
+ $event = $this->prophesize(SectionComponentBuildRenderArrayEvent::class);
+ // Assert that when not in preview, the event is ignored. We do this by
+ // asserting that in preview is called and nothing else.
+ $event->inPreview()->shouldBeCalled();
+
+ $component = new SectionComponent('', 'top');
+ $event->getComponent()->willReturn($component);
+ $subscriber->onBuildRender($event->reveal());
+ }
+
+ /**
+ * Ensure no conditions are applied when visibility isn't set.
+ *
+ * @covers ::onBuildRender
+ */
+ public function testOnBuildRenderNonPreviewEmpty() {
+ $subscriber = new SectionComponentVisibility(
+ new ContextHandler(),
+ $this->conditionManager
+ );
+ $event = $this->prophesize(SectionComponentBuildRenderArrayEvent::class);
+ // We're not in a preview.
+ $event->inPreview()->willReturn(FALSE);
+
+ $component = new SectionComponent('', 'top');
+ $event->getComponent()->willReturn($component);
+ $subscriber->onBuildRender($event->reveal());
+ }
+
+ /**
+ * Ensure no conditions are applied when visibility isn't set.
+ *
+ * @covers ::onBuildRender
+ */
+ public function testOnBuildRenderNonPreviewBadPlugin() {
+ $subscriber = new SectionComponentVisibility(
+ new ContextHandler(),
+ $this->conditionManager
+ );
+ $event = $this->prophesize(SectionComponentBuildRenderArrayEvent::class);
+ // We're not in a preview.
+ $event->inPreview()->willReturn(FALSE);
+
+ // Build a component so we can set properties.
+ $component = new SectionComponent('', 'top');
+ $component->set('visibility', [
+ 'uuid' => ['id' => 'plugin_dne'],
+ ]);
+ $event->getComponent()->willReturn($component);
+
+ $this->expectException(PluginNotFoundException::class);
+ $this->expectExceptionMessage('The "plugin_dne" plugin does not exist.');
+ $subscriber->onBuildRender($event->reveal());
+ }
+
+ /**
+ * Ensure context aware plugins get their context applied.
+ *
+ * @covers ::onBuildRender
+ */
+ public function testOnBuildRenderNonPreviewResolveContextAware() {
+ // Mock context aware plugin that will be used to assert we receive context.
+ $context_aware_plugin = $this->prophesize(ConditionInterface::class)
+ ->willImplement(ContextAwarePluginInterface::class);
+
+ // Set our mock condition manager to return the context plugin.
+ $condition_manager = $this->prophesize(ConditionManager::class);
+ $condition_manager->createInstance('context_aware', ['id' => 'context_aware'])
+ ->willReturn($context_aware_plugin->reveal());
+ $event = $this->prophesize(SectionComponentBuildRenderArrayEvent::class);
+ // We're not in a preview.
+ $event->inPreview()->willReturn(FALSE);
+
+ // Build a component with our "context_aware" plugin in the visibility.
+ $component = new SectionComponent('', 'top');
+ $component->set('visibility', [
+ 'uuid' => ['id' => 'context_aware'],
+ ]);
+ $event->getComponent()->willReturn($component);
+
+ // Setup a context array.
+ $context_definition = new ContextDefinition();
+ $context_data = StringData::createInstance(DataDefinition::create('string'));
+ $context_data->setValue('foo');
+ $context = [
+ 'bar' => new Context($context_definition, $context_data),
+ ];
+
+ // Make sure the context is returned by the event.
+ $event->getContexts()->willReturn($context);
+
+ // Steps that will be called in the process of resolving context.
+ $event->addCacheableDependency($context_aware_plugin)
+ ->shouldBeCalled();
+ $context_aware_plugin->setContext('bar', $context['bar'])
+ ->shouldBeCalled();
+ $context_aware_plugin->execute()->willReturn(TRUE);
+ $context_aware_plugin->getContextMapping()->willReturn([]);
+ $context_aware_plugin->getContext('bar')->willReturn(NULL);
+ // We want the "bar" context.
+ $context_aware_plugin->getContextDefinitions()
+ ->willReturn(['bar' => $context_definition]);
+
+ // Run the onBuildRender handler so things run.
+ (new SectionComponentVisibility(
+ new ContextHandler(),
+ $condition_manager->reveal()
+ ))->onBuildRender($event->reveal());
+ }
+
+ /**
+ * Ensure visibility plugins control event propagation.
+ *
+ * @covers ::onBuildRender
+ * @dataProvider buildRenderResolves
+ */
+ public function testOnBuildRenderNonPreviewResolve($result, $context_results) {
+ $uuid_factory = new UuidFactory();
+ $visibility_def = [];
+
+ $event = $this->prophesize(SectionComponentBuildRenderArrayEvent::class);
+
+ // Set our mock condition manager to return the context plugin.
+ $condition_manager = $this->prophesize(ConditionManager::class);
+ foreach ($context_results as $plugin_id => $plugin_result) {
+ $plugin = $this->prophesize(ConditionInterface::class);
+ $condition_manager->createInstance($plugin_id, ['id' => $plugin_id])
+ ->willReturn($plugin->reveal());
+ $plugin->execute()->willReturn($plugin_result);
+ $visibility_def[$uuid_factory->generate()] = ['id' => $plugin_id];
+ $event->addCacheableDependency($plugin)->shouldBeCalled();
+ }
+
+ // We're not in a preview.
+ $event->inPreview()->willReturn(FALSE);
+
+ // Build a component with our "context_aware" plugin in the visibility.
+ $component = new SectionComponent('', 'top');
+ $component->set('visibility', $visibility_def);
+ $event->getComponent()->willReturn($component);
+
+ // Assert different propagation outcomes.
+ if ($result) {
+ $event->stopPropagation()->shouldNotBeCalled();
+ }
+ else {
+ $event_plugin = $this->prophesize(PluginInspectionInterface::class)->reveal();
+ $event->stopPropagation()->shouldBeCalled();
+ $event->getPlugin()->willReturn($event_plugin);
+ }
+
+ // Run the onBuildRender handler so things run.
+ (new SectionComponentVisibility(
+ new ContextHandler(),
+ $condition_manager->reveal()
+ ))->onBuildRender($event->reveal());
+
+ }
+
+ /**
+ * Data Provider for testOnBuildRenderNonPreviewResolve().
+ *
+ * @return array
+ * Method parameters for testOnBuildRenderNonPreviewResolve().
+ */
+ public function buildRenderResolves() {
+ return [
+ [TRUE, ['foo' => TRUE, 'bar' => TRUE]],
+ [FALSE, ['foo' => TRUE, 'bar' => FALSE]],
+ [FALSE, ['foo' => TRUE, 'bar' => FALSE, 'biz' => TRUE]],
+ ];
+ }
+
+}