3 namespace Drupal\block;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Core\Plugin\PluginFormFactoryInterface;
7 use Drupal\Core\Block\BlockPluginInterface;
8 use Drupal\Core\Entity\EntityForm;
9 use Drupal\Core\Entity\EntityManagerInterface;
10 use Drupal\Core\Executable\ExecutableManagerInterface;
11 use Drupal\Core\Extension\ThemeHandlerInterface;
12 use Drupal\Core\Form\FormStateInterface;
13 use Drupal\Core\Form\SubformState;
14 use Drupal\Core\Language\LanguageManagerInterface;
15 use Drupal\Core\Plugin\ContextAwarePluginInterface;
16 use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
17 use Drupal\Core\Plugin\PluginWithFormsInterface;
18 use Symfony\Component\DependencyInjection\ContainerInterface;
21 * Provides form for block instance forms.
25 class BlockForm extends EntityForm {
30 * @var \Drupal\block\BlockInterface
37 * @var \Drupal\Core\Entity\EntityStorageInterface
42 * The condition plugin manager.
44 * @var \Drupal\Core\Condition\ConditionManager
49 * The event dispatcher service.
51 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
53 protected $dispatcher;
56 * The language manager service.
58 * @var \Drupal\Core\Language\LanguageManagerInterface
65 * @var \Drupal\Core\Extension\ThemeHandler
67 protected $themeHandler;
70 * The context repository service.
72 * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
74 protected $contextRepository;
77 * The plugin form manager.
79 * @var \Drupal\Core\Plugin\PluginFormFactoryInterface
81 protected $pluginFormFactory;
84 * Constructs a BlockForm object.
86 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
88 * @param \Drupal\Core\Executable\ExecutableManagerInterface $manager
89 * The ConditionManager for building the visibility UI.
90 * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
91 * The lazy context repository service.
92 * @param \Drupal\Core\Language\LanguageManagerInterface $language
93 * The language manager.
94 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
96 * @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
97 * The plugin form manager.
99 public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler, PluginFormFactoryInterface $plugin_form_manager) {
100 $this->storage = $entity_manager->getStorage('block');
101 $this->manager = $manager;
102 $this->contextRepository = $context_repository;
103 $this->language = $language;
104 $this->themeHandler = $theme_handler;
105 $this->pluginFormFactory = $plugin_form_manager;
111 public static function create(ContainerInterface $container) {
113 $container->get('entity.manager'),
114 $container->get('plugin.manager.condition'),
115 $container->get('context.repository'),
116 $container->get('language_manager'),
117 $container->get('theme_handler'),
118 $container->get('plugin_form.factory')
125 public function form(array $form, FormStateInterface $form_state) {
126 $entity = $this->entity;
128 // Store theme settings in $form_state for use below.
129 if (!$theme = $entity->getTheme()) {
130 $theme = $this->config('system.theme')->get('default');
132 $form_state->set('block_theme', $theme);
134 // Store the gathered contexts in the form state for other objects to use
135 // during form building.
136 $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
138 $form['#tree'] = TRUE;
139 $form['settings'] = [];
140 $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
141 $form['settings'] = $this->getPluginForm($entity->getPlugin())->buildConfigurationForm($form['settings'], $subform_state);
142 $form['visibility'] = $this->buildVisibilityInterface([], $form_state);
144 // If creating a new block, calculate a safe default machine name.
146 '#type' => 'machine_name',
148 '#description' => $this->t('A unique name for this block instance. Must be alpha-numeric and underscore separated.'),
149 '#default_value' => !$entity->isNew() ? $entity->id() : $this->getUniqueMachineName($entity),
151 'exists' => '\Drupal\block\Entity\Block::load',
152 'replace_pattern' => '[^a-z0-9_.]+',
153 'source' => ['settings', 'label'],
156 '#disabled' => !$entity->isNew(),
160 if ($entity->getTheme()) {
168 foreach ($this->themeHandler->listInfo() as $theme_name => $theme_info) {
169 if (!empty($theme_info->status)) {
170 $theme_options[$theme_name] = $theme_info->info['name'];
175 '#options' => $theme_options,
176 '#title' => t('Theme'),
177 '#default_value' => $theme,
179 'callback' => '::themeSwitch',
180 'wrapper' => 'edit-block-region-wrapper',
185 // Hidden weight setting.
186 $weight = $entity->isNew() ? $this->getRequest()->query->get('weight', 0) : $entity->getWeight();
189 '#default_value' => $weight,
193 $entity_region = $entity->getRegion();
194 $region = $entity->isNew() ? $this->getRequest()->query->get('region', $entity_region) : $entity_region;
197 '#title' => $this->t('Region'),
198 '#description' => $this->t('Select the region where this block should be displayed.'),
199 '#default_value' => $region,
201 '#options' => system_region_list($theme, REGIONS_VISIBLE),
202 '#prefix' => '<div id="edit-block-region-wrapper">',
203 '#suffix' => '</div>',
205 $form['#attached']['library'][] = 'block/drupal.block.admin';
210 * Handles switching the available regions based on the selected theme.
212 public function themeSwitch($form, FormStateInterface $form_state) {
213 $form['region']['#options'] = system_region_list($form_state->getValue('theme'), REGIONS_VISIBLE);
214 return $form['region'];
218 * Helper function for building the visibility UI form.
221 * An associative array containing the structure of the form.
222 * @param \Drupal\Core\Form\FormStateInterface $form_state
223 * The current state of the form.
226 * The form array with the visibility UI added in.
228 protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) {
229 $form['visibility_tabs'] = [
230 '#type' => 'vertical_tabs',
231 '#title' => $this->t('Visibility'),
232 '#parents' => ['visibility_tabs'],
235 'block/drupal.block',
239 // @todo Allow list of conditions to be configured in
240 // https://www.drupal.org/node/2284687.
241 $visibility = $this->entity->getVisibility();
242 $definitions = $this->manager->getFilteredDefinitions('block_ui', $form_state->getTemporaryValue('gathered_contexts'), ['block' => $this->entity]);
243 foreach ($definitions as $condition_id => $definition) {
244 // Don't display the current theme condition.
245 if ($condition_id == 'current_theme') {
248 // Don't display the language condition until we have multiple languages.
249 if ($condition_id == 'language' && !$this->language->isMultilingual()) {
252 /** @var \Drupal\Core\Condition\ConditionInterface $condition */
253 $condition = $this->manager->createInstance($condition_id, isset($visibility[$condition_id]) ? $visibility[$condition_id] : []);
254 $form_state->set(['conditions', $condition_id], $condition);
255 $condition_form = $condition->buildConfigurationForm([], $form_state);
256 $condition_form['#type'] = 'details';
257 $condition_form['#title'] = $condition->getPluginDefinition()['label'];
258 $condition_form['#group'] = 'visibility_tabs';
259 $form[$condition_id] = $condition_form;
262 if (isset($form['node_type'])) {
263 $form['node_type']['#title'] = $this->t('Content types');
264 $form['node_type']['bundles']['#title'] = $this->t('Content types');
265 $form['node_type']['negate']['#type'] = 'value';
266 $form['node_type']['negate']['#title_display'] = 'invisible';
267 $form['node_type']['negate']['#value'] = $form['node_type']['negate']['#default_value'];
269 if (isset($form['user_role'])) {
270 $form['user_role']['#title'] = $this->t('Roles');
271 unset($form['user_role']['roles']['#description']);
272 $form['user_role']['negate']['#type'] = 'value';
273 $form['user_role']['negate']['#value'] = $form['user_role']['negate']['#default_value'];
275 if (isset($form['request_path'])) {
276 $form['request_path']['#title'] = $this->t('Pages');
277 $form['request_path']['negate']['#type'] = 'radios';
278 $form['request_path']['negate']['#default_value'] = (int) $form['request_path']['negate']['#default_value'];
279 $form['request_path']['negate']['#title_display'] = 'invisible';
280 $form['request_path']['negate']['#options'] = [
281 $this->t('Show for the listed pages'),
282 $this->t('Hide for the listed pages'),
285 if (isset($form['language'])) {
286 $form['language']['negate']['#type'] = 'value';
287 $form['language']['negate']['#value'] = $form['language']['negate']['#default_value'];
295 protected function actions(array $form, FormStateInterface $form_state) {
296 $actions = parent::actions($form, $form_state);
297 $actions['submit']['#value'] = $this->t('Save block');
298 $actions['delete']['#title'] = $this->t('Remove block');
305 public function validateForm(array &$form, FormStateInterface $form_state) {
306 parent::validateForm($form, $form_state);
308 $form_state->setValue('weight', (int) $form_state->getValue('weight'));
309 // The Block Entity form puts all block plugin form elements in the
310 // settings form element, so just pass that to the block for validation.
311 $this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
312 $this->validateVisibility($form, $form_state);
316 * Helper function to independently validate the visibility UI.
319 * A nested array form elements comprising the form.
320 * @param \Drupal\Core\Form\FormStateInterface $form_state
321 * The current state of the form.
323 protected function validateVisibility(array $form, FormStateInterface $form_state) {
324 // Validate visibility condition settings.
325 foreach ($form_state->getValue('visibility') as $condition_id => $values) {
326 // All condition plugins use 'negate' as a Boolean in their schema.
327 // However, certain form elements may return it as 0/1. Cast here to
328 // ensure the data is in the expected type.
329 if (array_key_exists('negate', $values)) {
330 $form_state->setValue(['visibility', $condition_id, 'negate'], (bool) $values['negate']);
333 // Allow the condition to validate the form.
334 $condition = $form_state->get(['conditions', $condition_id]);
335 $condition->validateConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state));
342 public function submitForm(array &$form, FormStateInterface $form_state) {
343 parent::submitForm($form, $form_state);
345 $entity = $this->entity;
346 // The Block Entity form puts all block plugin form elements in the
347 // settings form element, so just pass that to the block for submission.
348 $sub_form_state = SubformState::createForSubform($form['settings'], $form, $form_state);
349 // Call the plugin submit handler.
350 $block = $entity->getPlugin();
351 $this->getPluginForm($block)->submitConfigurationForm($form, $sub_form_state);
352 // If this block is context-aware, set the context mapping.
353 if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) {
354 $context_mapping = $sub_form_state->getValue('context_mapping', []);
355 $block->setContextMapping($context_mapping);
358 $this->submitVisibility($form, $form_state);
360 // Save the settings of the plugin.
363 $this->messenger()->addStatus($this->t('The block configuration has been saved.'));
364 $form_state->setRedirect(
365 'block.admin_display_theme',
367 'theme' => $form_state->getValue('theme'),
369 ['query' => ['block-placement' => Html::getClass($this->entity->id())]]
374 * Helper function to independently submit the visibility UI.
377 * A nested array form elements comprising the form.
378 * @param \Drupal\Core\Form\FormStateInterface $form_state
379 * The current state of the form.
381 protected function submitVisibility(array $form, FormStateInterface $form_state) {
382 foreach ($form_state->getValue('visibility') as $condition_id => $values) {
383 // Allow the condition to submit the form.
384 $condition = $form_state->get(['conditions', $condition_id]);
385 $condition->submitConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state));
387 // Setting conditions' context mappings is the plugins' responsibility.
388 // This code exists for backwards compatibility, because
389 // \Drupal\Core\Condition\ConditionPluginBase::submitConfigurationForm()
390 // did not set its own mappings until Drupal 8.2
391 // @todo Remove the code that sets context mappings in Drupal 9.0.0.
392 if ($condition instanceof ContextAwarePluginInterface) {
393 $context_mapping = isset($values['context_mapping']) ? $values['context_mapping'] : [];
394 $condition->setContextMapping($context_mapping);
397 $condition_configuration = $condition->getConfiguration();
398 // Update the visibility conditions on the block.
399 $this->entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration);
404 * Generates a unique machine name for a block.
406 * @param \Drupal\block\BlockInterface $block
410 * Returns the unique name.
412 public function getUniqueMachineName(BlockInterface $block) {
413 $suggestion = $block->getPlugin()->getMachineNameSuggestion();
415 // Get all the blocks which starts with the suggested machine name.
416 $query = $this->storage->getQuery();
417 $query->condition('id', $suggestion, 'CONTAINS');
418 $block_ids = $query->execute();
420 $block_ids = array_map(function ($block_id) {
421 $parts = explode('.', $block_id);
425 // Iterate through potential IDs until we get a new one. E.g.
426 // 'plugin', 'plugin_2', 'plugin_3', etc.
428 $machine_default = $suggestion;
429 while (in_array($machine_default, $block_ids)) {
430 $machine_default = $suggestion . '_' . ++$count;
432 return $machine_default;
436 * Retrieves the plugin form for a given block and operation.
438 * @param \Drupal\Core\Block\BlockPluginInterface $block
441 * @return \Drupal\Core\Plugin\PluginFormInterface
442 * The plugin form for the block.
444 protected function getPluginForm(BlockPluginInterface $block) {
445 if ($block instanceof PluginWithFormsInterface) {
446 return $this->pluginFormFactory->createInstance($block, 'configure');