diff --git a/core/composer.json b/core/composer.json index 53ab8fa..862b5a3 100644 --- a/core/composer.json +++ b/core/composer.json @@ -94,6 +94,7 @@ "drupal/editor": "self.version", "drupal/entity_reference": "self.version", "drupal/field": "self.version", + "drupal/field_layout": "self.version", "drupal/field_ui": "self.version", "drupal/file": "self.version", "drupal/filter": "self.version", diff --git a/core/modules/field_layout/config/schema/field_layout.schema.yml b/core/modules/field_layout/config/schema/field_layout.schema.yml new file mode 100644 index 0000000..3460b05 --- /dev/null +++ b/core/modules/field_layout/config/schema/field_layout.schema.yml @@ -0,0 +1,13 @@ +core.entity_view_display.*.*.*.third_party.field_layout: + type: field_layout.third_party_settings + +core.entity_form_display.*.*.*.third_party.field_layout: + type: field_layout.third_party_settings + +field_layout.third_party_settings: + type: mapping + label: 'Per-view mode field layout settings' + mapping: + layout: + type: string + label: 'Layout' diff --git a/core/modules/field_layout/css/twocol.layout.css b/core/modules/field_layout/css/twocol.layout.css new file mode 100644 index 0000000..6803d5b --- /dev/null +++ b/core/modules/field_layout/css/twocol.layout.css @@ -0,0 +1,8 @@ +.field-layout--twocol { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} +.field-layout--twocol > .field-layout-region { + flex: 0 1 50%; +} diff --git a/core/modules/field_layout/field_layout.info.yml b/core/modules/field_layout/field_layout.info.yml new file mode 100644 index 0000000..62f225a --- /dev/null +++ b/core/modules/field_layout/field_layout.info.yml @@ -0,0 +1,6 @@ +name: 'Field Layout' +type: module +description: 'Adds layout capabilities to the Field UI.' +package: Core (Experimental) +version: VERSION +core: 8.x diff --git a/core/modules/field_layout/field_layout.install b/core/modules/field_layout/field_layout.install new file mode 100644 index 0000000..1d935bc --- /dev/null +++ b/core/modules/field_layout/field_layout.install @@ -0,0 +1,26 @@ +save(); + }; + array_map($entity_save, EntityViewDisplay::loadMultiple()); + array_map($entity_save, EntityFormDisplay::loadMultiple()); + + // Invalidate the render cache since all content will now have a layout. + Cache::invalidateTags(['rendered']); +} diff --git a/core/modules/field_layout/field_layout.libraries.yml b/core/modules/field_layout/field_layout.libraries.yml new file mode 100644 index 0000000..9798509 --- /dev/null +++ b/core/modules/field_layout/field_layout.libraries.yml @@ -0,0 +1,5 @@ +drupal.field_layout.twocol: + version: VERSION + css: + layout: + css/twocol.layout.css: {} diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module new file mode 100644 index 0000000..17f8c27 --- /dev/null +++ b/core/modules/field_layout/field_layout.module @@ -0,0 +1,168 @@ +' . t('About') . ''; + $output .= '
' . t('The Field Layout module allows you to arrange fields into regions for content forms and displays.') . '
'; + $output .= '' . t('For more information, see the online documentation for the Field Layout module.', [':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/@todo_once_module_name_is_decided_upon']) . '
'; + return $output; + } +} + +/** + * Implements hook_entity_type_alter(). + */ +function field_layout_entity_type_alter(array &$entity_types) { + /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ + if (\Drupal::moduleHandler()->moduleExists('field_ui')) { + $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class); + $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class); + $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class); + $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class); + } +} + +/** + * Implements hook_entity_presave(). + */ +function field_layout_entity_presave(EntityInterface $entity) { + // Whenever creating a new entity display, set the layout to default. + if ($entity instanceof EntityDisplayInterface && !$entity->getThirdPartySettings('field_layout')) { + $entity->setThirdPartySetting('field_layout', 'layout', 'default'); + } +} + +/** + * Implements hook_entity_view_alter(). + */ +function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + _field_layout_apply_layout($build, $display); +} + +/** + * Implements hook_form_alter(). + */ +function field_layout_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $form_object = $form_state->getFormObject(); + if ($form_object instanceof ContentEntityFormInterface) { + _field_layout_apply_layout($form, $form_object->getFormDisplay($form_state), TRUE); + } +} + +/** + * Applies the layout to an entity build. + * + * @param array $build + * A renderable array representing the entity content or form. + * @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display + * The entity display holding the display options configured for the + * entity components. + * @param bool $in_form_context + * (optional) If in a form context, an alternate method will be used to render + * fields in their regions. Defaults to FALSE. + */ +function _field_layout_apply_layout(array &$build, EntityDisplayInterface $display, $in_form_context = FALSE) { + if (!$layout_definition = \Drupal::service('field_layout.layout_repository')->getLayoutForDisplay($display)) { + return; + } + + // Wrap the regions in a layout element. + $build['field_layout']['#theme_wrappers'][] = 'field_layout_layout'; + $build['field_layout']['#layout'] = $layout_definition['layout']; + if (isset($layout_definition['library'])) { + $build['#attached']['library'][] = $layout_definition['library']; + } + + // Add the regions to the $build in the correct order. + foreach ($layout_definition['regions'] as $region => $region_info) { + $build['field_layout']['field_layout__' . $region]['#theme_wrappers'][] = 'field_layout_region'; + $build['field_layout']['field_layout__' . $region]['#region'] = $region; + } + + /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */ + $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle()); + // Move the field from the top-level of $build into a region-specific section. + foreach ($display->getComponents() as $name => $field) { + // If the component is a true field, but not configurable, do not alter it. + if (isset($field_definitions[$name]) && !$field_definitions[$name]->isDisplayConfigurable($display->get('displayContext'))) { + continue; + } + + if (isset($build[$name]) && isset($field['region'])) { + // If this is a form, #group can be used to relocate the fields. This + // avoids breaking hook_form_alter() implementations by not actually + // moving the field in the form structure. + if ($in_form_context) { + $build[$name]['#group'] = 'field_layout__' . $field['region']; + } + else { + $build['field_layout']['field_layout__' . $field['region']][$name] = $build[$name]; + unset($build[$name]); + } + } + } +} + +/** + * Implements hook_theme(). + */ +function field_layout_theme($existing, $type, $theme, $path) { + return [ + 'field_layout_region' => [ + 'render element' => 'elements', + ], + 'field_layout_layout' => [ + 'render element' => 'elements', + ], + ]; +} + +/** + * Prepares variables for field layout layout templates. + * + * Default template: field-layout-layout.html.twig. + * + * @param array $variables + * An associative array containing: + * - elements: An associative array containing properties of the layout. + */ +function template_preprocess_field_layout_layout(&$variables) { + // Create the $content variable that templates expect. + $variables['content'] = $variables['elements']['#children']; + $variables['layout'] = $variables['elements']['#layout']; +} + +/** + * Prepares variables for field layout region templates. + * + * Default template: field-layout-region.html.twig. + * + * @param array $variables + * An associative array containing: + * - elements: An associative array containing properties of the region. + */ +function template_preprocess_field_layout_region(&$variables) { + // Create the $content variable that templates expect. + $variables['content'] = $variables['elements']['#children']; + $variables['region'] = $variables['elements']['#region']; +} diff --git a/core/modules/field_layout/field_layout.services.yml b/core/modules/field_layout/field_layout.services.yml new file mode 100644 index 0000000..b4082ff --- /dev/null +++ b/core/modules/field_layout/field_layout.services.yml @@ -0,0 +1,4 @@ +services: + field_layout.layout_repository: + class: Drupal\field_layout\LayoutRepository + arguments: ['@string_translation'] diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php new file mode 100644 index 0000000..679d26c --- /dev/null +++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php @@ -0,0 +1,18 @@ +getLayoutForDisplay($this); + return key($layout_definition['regions']); + } + +} diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php new file mode 100644 index 0000000..620d63d --- /dev/null +++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php @@ -0,0 +1,14 @@ +fieldLayoutRepository->getLayoutForDisplay($this->getEntity()); + foreach ($layout['regions'] as $name => $region) { + $regions[$name] = [ + 'title' => $region['label'], + 'message' => $this->t('No field is displayed.') + ]; + } + + $regions['hidden'] = [ + 'title' => $this->t('Disabled', [], ['context' => 'Plural']), + 'message' => $this->t('No field is hidden.') + ]; + + return $regions; + } + + /** + * Adds the field layout section to the form. + * + * @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 array + */ + protected function addFieldLayout(array $form, FormStateInterface $form_state) { + $form['#entity_builders'][] = [static::class, 'updateFieldLayout']; + + $form['field_layouts'] = [ + '#type' => 'details', + '#title' => $this->t('Layout for @bundle in @view_mode', [ + '@bundle' => str_replace('_', ' ', $this->getEntity()->getTargetBundle()), + '@view_mode' => str_replace('_', ' ', $this->getEntity()->getMode()), + ]), + ]; + $layout_options = []; + foreach ($this->fieldLayoutRepository->getLayoutDefinitions() as $name => $layout) { + $layout_options[$name] = $layout['label']; + } + $form['field_layouts']['field_layout'] = [ + '#type' => 'select', + '#title' => $this->t('Select a layout'), + '#options' => $layout_options, + '#default_value' => $this->getEntity()->getThirdPartySetting('field_layout', 'layout', 'default'), + ]; + + return $form; + } + + /** + * An #entity_builder callback to update the Field Layout settings. + * + * @param string $entity_type_id + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public static function updateFieldLayout($entity_type_id, EntityInterface $entity, &$form, FormStateInterface $form_state) { + if ($form_state->isSubmitted() && $entity instanceof EntityDisplayWithLayoutInterface) { + $old_layout = $entity->getThirdPartySetting('field_layout', 'layout'); + $new_layout = $form_state->getValue('field_layout'); + $entity->setThirdPartySetting('field_layout', 'layout', $new_layout); + // If the layout is changing, reset all fields. + if ($new_layout !== $old_layout) { + // @todo Devise a mechanism for mapping old regions to new ones in + // https://www.drupal.org/node/2796877. + $new_region = $entity->getDefaultRegion(); + foreach ($form_state->getValue('fields') as $field_name => $values) { + if (($component = $entity->getComponent($field_name)) && $new_region !== 'hidden') { + $component['region'] = $new_region; + $entity->setComponent($field_name, $component); + } + else { + $entity->removeComponent($field_name); + } + } + } + } + } + + /** + * Gets the form entity. + * + * @return \Drupal\Core\Entity\Display\EntityDisplayInterface + * The current form entity. + */ + abstract public function getEntity(); + +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php new file mode 100644 index 0000000..d57912e --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php @@ -0,0 +1,54 @@ +fieldLayoutRepository = $field_layout_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.field.field_type'), + $container->get('plugin.manager.field.widget'), + $container->get('field_layout.layout_repository') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + return $this->addFieldLayout($form, $form_state); + } + +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php new file mode 100644 index 0000000..5c9f26f --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php @@ -0,0 +1,54 @@ +fieldLayoutRepository = $field_layout_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.field.field_type'), + $container->get('plugin.manager.field.formatter'), + $container->get('field_layout.layout_repository') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + return $this->addFieldLayout($form, $form_state); + } + +} diff --git a/core/modules/field_layout/src/LayoutRepository.php b/core/modules/field_layout/src/LayoutRepository.php new file mode 100644 index 0000000..5fe4540 --- /dev/null +++ b/core/modules/field_layout/src/LayoutRepository.php @@ -0,0 +1,72 @@ +stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function getLayoutDefinitions() { + // @todo Replace with layout_plugin in https://www.drupal.org/node/2296423. + $layouts = [ + 'default' => [ + 'label' => $this->t('Default'), + 'regions' => [ + 'content' => [ + 'label' => $this->t('Content'), + ], + ], + ], + 'twocol' => [ + 'label' => $this->t('Two column'), + 'library' => 'field_layout/drupal.field_layout.twocol', + 'regions' => [ + 'left' => [ + 'label' => $this->t('Left'), + ], + 'right' => [ + 'label' => $this->t('Right'), + ], + ], + ], + ]; + foreach ($layouts as $layout_name => $layout) { + $layouts[$layout_name]['layout'] = $layout_name; + } + return $layouts; + } + + /** + * {@inheritdoc} + */ + public function getLayoutForDisplay(EntityDisplayInterface $display) { + $layout_name = $display->getThirdPartySetting('field_layout', 'layout', 'default'); + $layout_definitions = $this->getLayoutDefinitions(); + if (!$layout_name || !isset($layout_definitions[$layout_name])) { + return []; + } + + return $layout_definitions[$layout_name]; + } + +} diff --git a/core/modules/field_layout/src/LayoutRepositoryInterface.php b/core/modules/field_layout/src/LayoutRepositoryInterface.php new file mode 100644 index 0000000..f8287f3 --- /dev/null +++ b/core/modules/field_layout/src/LayoutRepositoryInterface.php @@ -0,0 +1,31 @@ +. + * - layout: The name of the layout variable. + * + * @ingroup themeable + */ +#} +{% +set classes = [ +'field-layout--' ~ layout|clean_class, +] +%} +{% if content %} +