You are here

entity_reference_layout.module in Entity Reference with Layout 8

File

entity_reference_layout.module
View source
<?php

/**
 * @file
 * Contains entity_reference_layout.module.
 */
use Drupal\Core\Url;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\entity_reference_layout\Event\ErlMergeAttributesEvent;
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_help().
 */
function entity_reference_layout_help($route_name, RouteMatchInterface $route_match) {
  $output = '';
  switch ($route_name) {

    // Main module help for the entity_reference_layout module.
    case 'help.page.entity_reference_layout':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Entity reference field with layouts') . '</p>';
      break;
  }
  return $output;
}

/**
 * Implements hook_theme().
 */
function entity_reference_layout_theme() {
  return [
    'entity_reference_layout_widget' => [
      'render element' => 'form',
      'function' => 'theme_entity_reference_layout_widget',
    ],
    'entity_reference_layout_radio' => [
      'render element' => 'element',
      'function' => 'theme_entity_reference_layout_radio',
    ],
    'entity_reference_layout' => [
      'variables' => [
        'elements' => '',
        'content' => '',
      ],
    ],
  ];
}

/**
 * Implements hook_theme_suggestions().
 */
function entity_reference_layout_theme_suggestions_entity_reference_layout(array $variables) {
  $suggestions = [];
  $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
  $suggestions[] = 'entity_reference_layout__' . $sanitized_view_mode;
  $suggestions[] = 'entity_reference_layout__' . $variables['elements']['field_name'];
  $suggestions[] = 'entity_reference_layout__' . $variables['elements']['field_name'] . '__' . $sanitized_view_mode;
  return $suggestions;
}

/**
 * Implements hook_entity_reference_layout_radio().
 *
 * Custom theme hook for adding layout icons
 * and wrapper HTML to layout select radios.
 */
function theme_entity_reference_layout_radio($element) {

  /* @var \Drupal\Core\Layout\LayoutPluginManager $layout_plugin_manager */
  $layout_plugin_manager = \Drupal::service('plugin.manager.core.layout');
  $renderer = \Drupal::service('renderer');
  $layout_name = $element['element']['#return_value'];
  try {

    /* @var \Drupal\Core\Layout\LayoutDefinition $definition */
    $definition = $layout_plugin_manager
      ->getDefinition($layout_name);
    $icon = $definition
      ->getIcon(40, 60, 1, 0);
    $rendered_icon = $renderer
      ->render($icon);
    $layout_item = [
      '#type' => 'container',
      '#prefix' => '<div class="layout-radio-item">',
      '#suffix' => '</div>',
      'icon' => [
        '#prefix' => '<div class="layout-icon-wrapper">',
        '#suffix' => '</div>',
        '#markup' => $rendered_icon,
      ],
      'radio' => [
        '#type' => 'container',
        '#attributes' => [],
        'item' => [
          '#markup' => $element['element']['#children'],
        ],
      ],
    ];
    return \Drupal::service('renderer')
      ->render($layout_item);
  } catch (\Exception $e) {
    watchdog_exception('entity_reference_layout', $e);
  }
  return [];
}

/**
 * Merges $layout_options into an $attributes array.
 *
 * Returned attributes are passed to a rendered layout,
 * typically with custom classes to be applied although can
 * include other data useful to rendering.
 *
 * Leverages even dispatcher pattern so other modules
 * can add data to attributes.
 */
function entity_reference_layout_merge_attributes(array $attributes, array $layout_options) {
  if (!empty($layout_options)) {
    if (!empty($layout_options['options']['container_classes'])) {
      $attributes['class'][] = $layout_options['options']['container_classes'];
    }
    if (!empty($layout_options['options']['bg_color'])) {
      $attributes['style'] = [
        'background-color: ' . $layout_options['options']['bg_color'],
      ];
    }
  }
  $event = new ErlMergeAttributesEvent($attributes, $layout_options);
  $event_dispatcher = \Drupal::service('event_dispatcher');
  $event_dispatcher
    ->dispatch(ErlMergeAttributesEvent::EVENT_NAME, $event);
  return $attributes;
}

/**
 * Helper function to sort array items by '_weight'.
 */
function _erl_widget_sort_helper($a, $b) {
  $a_weight = is_array($a) && isset($a['_weight']['#value']) ? $a['_weight']['#value'] : 0;
  $b_weight = is_array($b) && isset($b['_weight']['#value']) ? $b['_weight']['#value'] : 0;
  return $a_weight - $b_weight;
}

/**
 * Themes the "ERL" field widget.
 *
 * @param array $variables
 *   Contains the form element data from $element['entities'].
 */
function theme_entity_reference_layout_widget(array $variables) {
  $currentUser = \Drupal::service('current_user');
  $form = $variables['form'];
  $build = [
    '#type' => 'fieldset',
    '#id' => $form['#id'],
    '#attributes' => [
      'class' => [
        'erl-field',
      ],
    ],
    '#title' => $form['#title'],
    // These get moved around with JS.
    'add_more' => [
      '#weight' => 998,
    ] + $form['add_more'],
    // Container for layouts.
    'layout_items' => [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'erl-layout-wrapper',
        ],
      ],
    ],
    // Container for disabled / orphaned items.
    'disabled' => [
      '#type' => 'fieldset',
      '#attributes' => [
        'class' => [
          'erl-disabled-items',
        ],
      ],
      '#weight' => 999,
      '#title' => t('Disabled Items'),
      'items' => [
        '#type' => 'container',
        '#attributes' => [
          'class' => [
            'erl-disabled-wrapper',
          ],
        ],
        'description' => [
          '#type' => 'html_tag',
          '#tag' => 'div',
          '#attributes' => [
            'class' => [
              'erl-disabled-items__description',
            ],
          ],
          '#value' => t('Drop items here that you want to keep disabled / hidden, without removing them permanently.'),
        ],
      ],
    ],
  ];

  // Add a Warning Message if this is a Translation Context.
  if (!empty($build["add_more"]["actions"]["#isTranslating"])) {
    $build['is_translating_warning'] = [
      '#type' => "html_tag",
      '#tag' => 'div',
      '#value' => t("This is Translation Context (editing a version not in the original language). <b>No new Layout Sections and Items can be added</b>."),
      '#weight' => -1100,
      '#attributes' => [
        'class' => [
          'is_translating_warning',
        ],
      ],
    ];
  }
  $new_items = [];
  $form_with_layout = [];
  $renderer = \Drupal::service('renderer');

  /* @var \Drupal\Core\Layout\LayoutPluginManager $layout_plugin_manager */
  $layout_plugin_manager = \Drupal::service('plugin.manager.core.layout');
  $items = [];
  foreach (Element::children($form) as $key) {
    if ($key !== 'add_more') {

      // If there is an open entity form,
      // move it to it's own section of the widget form.
      if (!empty($form[$key]['entity_form'])) {
        $build['entity_form'] = $form[$key]['entity_form'] + [
          '#weight' => 1000,
        ];
        $build['entity_form']['#attributes']['class'][] = 'erl-entity-form';
        unset($form[$key]['entity_form']);
      }

      // If there is an open remove confirmation form,
      // move it to it's own section of the widget form.
      if (!empty($form[$key]['remove_form'])) {
        $build['remove_form'] = $form[$key]['remove_form'] + [
          '#weight' => 1000,
        ];
        unset($form[$key]['remove_form']);
      }
      $items[] =& $form[$key];
    }
  }
  usort($items, '_erl_widget_sort_helper');
  $has_non_layout_items = FALSE;
  $section_key = NULL;
  foreach ($items as $key => &$item) {

    // Merge and add attributes.
    $attributes = entity_reference_layout_merge_attributes($item['#attributes'], $item['#layout_options']);
    $item['#attributes'] = $attributes;

    // Set the weight for sorted list items.
    $item['#weight'] = $key;

    // If this is a layout we'll populate regions below.
    $item['#regions'] = [
      '#attributes' => [
        'class' => [],
      ],
    ];

    // Add class for weight element.
    if (!empty($item['_weight'])) {
      $item['_weight']['#attributes']['class'] = [
        'erl-weight',
      ];

      // Move to top of container to ensure items are given the correct delta.
      $item['_weight']['#weight'] = -1000;
      $item['_weight']['#theme_wrappers'] = [
        'container' => [
          '#attributes' => [
            'class' => [
              'hidden',
            ],
          ],
        ],
      ];
    }

    // Stash new items for processing later.
    if (!empty($item['#is_new'])) {
      $new_items[] = $item;
    }
    elseif (!empty($item['layout']['#value'])) {
      $section_key = $key;
      $form_with_layout['section_' . $section_key] = $items[$section_key];
      $form_with_layout['section_' . $section_key]['#attributes']['class'][] = 'erl-layout';
      try {
        $layout_instance = $layout_plugin_manager
          ->createInstance($items[$section_key]['layout']['#value'], $items[$section_key]['config']['#value']);
        foreach ($layout_instance
          ->getPluginDefinition()
          ->getRegionNames() as $region_name) {
          $form_with_layout['section_' . $section_key]['#regions'][$region_name] = [
            '#attributes' => [
              'class' => [
                'erl-layout-region',
                'erl-layout-region--' . $region_name,
              ],
            ],
          ];
        }
      } catch (\Exception $e) {
        watchdog_exception('Erl, Layout Plugin Manager', $e);
      }
    }
    elseif (!empty($item['region']['#value']) && isset($form_with_layout['section_' . $section_key]['#regions'][$item['region']['#value']])) {
      $form_with_layout['section_' . $section_key]['#regions'][$item['region']['#value']][] = $item;
      $has_non_layout_items = TRUE;
    }
    else {
      $build['disabled']['items'][] = $item;
      $has_non_layout_items = TRUE;
    }
  }

  // Move new items into correct position, if applicable.
  foreach ($new_items as $key => $item) {
    $section_key = $item['#parent_weight'];

    // Layout items.
    if (empty($item['#new_region'])) {
      $item['#weight'] = $item['#parent_weight'] + 0.5;
      $form_with_layout['new_section_' . $key] = $item;
    }

    // Items to add into regions.
    if (isset($item['#new_region'])) {
      $region_name = $item['#new_region'];
      if (isset($form_with_layout['section_' . $section_key]['#regions'][$region_name])) {
        $form_with_layout['section_' . $section_key]['#regions'][$region_name][] = $item;
      }
    }
  }
  $show_layout_labels = \Drupal::config('entity_reference_layout.settings')
    ->get('show_layout_labels');
  foreach ($form_with_layout as $key => $section) {
    if (!empty($section['layout']['#value'])) {
      try {
        $layout_instance = $layout_plugin_manager
          ->createInstance($section['layout']['#value'], $section['config']['#value']);

        // Add a "Add Item" button, if is not a translation context.
        if (empty($build["add_more"]["actions"]["#isTranslating"])) {
          foreach (array_keys($section['#regions']) as $region) {
            $section['#regions'][$region]['button'] = [
              '#markup' => '<button class="erl-add-content__toggle">+<span class="visually-hidden">' . t('Add Item') . '</span></button>',
              '#allowed_tags' => [
                'button',
                'span',
              ],
              // Toggle is always last in region.
              '#weight' => 10000,
            ];
          }
        }
        $rendered_regions = $layout_instance
          ->build($section['#regions']);
        $section['#regions']['#attributes']['class'][] = 'erl-layout__regions';
        if ($show_layout_labels === 1) {
          $label = $layout_instance
            ->getPluginDefinition() ? $layout_instance
            ->getPluginDefinition()
            ->getLabel()
            ->__toString() : $section['#layout'];
          $section['layout']['label'] = [
            '#type' => 'label',
            '#title' => $label,
            '#title_display' => $label,
            '#attributes' => [
              'class' => [
                'paragraph-layout-label',
              ],
            ],
          ];
        }
        $section['preview']['content'] = [
          '#weight' => 1000,
          'regions' => $rendered_regions,
        ];
      } catch (\Exception $e) {
        watchdog_exception('Erl, Layout Plugin Manager', $e);
      }

      // Add a "Add Section" button, if is not a translation context.
      if ($currentUser
        ->hasPermission('manage entity reference layout sections') && empty($build["add_more"]["actions"]["#isTranslating"])) {

        // Add the "add section" button for js.
        $section['button'] = [
          '#markup' => '<div class="erl-add-content--single"><button class="erl-add-section"><span class="icon">+</span>' . t('Add Section') . '</button></div>',
          '#allowed_tags' => [
            'button',
            'span',
            'div',
          ],
        ];
      }
    }
    $build['layout_items'][] = $section;
  }
  if (count($form_with_layout) == 0) {
    $build['add_section_button'] = [
      '#markup' => '<div class="erl-first-section"><div class="erl-add-content--single"><button class="erl-add-section"><span class="icon">+</span>' . t('Add Section') . '</button></div></div>',
      '#allowed_tags' => [
        'button',
        'span',
        'div',
      ],
      '#weight' => -1,
    ];
  }
  $show_labels = \Drupal::config('entity_reference_layout.settings')
    ->get('show_paragraph_labels');
  if ($show_labels) {
    foreach ($build["layout_items"] as $key => $layout_item) {
      if (!isset($layout_item["preview"]["content"]["regions"])) {
        continue;
      }
      foreach ($build['layout_items'][$key]['preview']['content']['regions'] as $regionKey => $region) {
        if (!is_array($region)) {
          continue;
        }
        foreach ($region as $paragraphKey => $paragraph) {
          if (!isset($paragraph['#entity'])) {
            continue;
          }
          $entity = $paragraph['#entity'];
          $label = $entity
            ->getParagraphType()->label;
          $build['layout_items'][$key]['preview']['content']['regions'][$regionKey][$paragraphKey][] = [
            '#type' => 'label',
            '#title' => $label,
            '#title_display' => $label,
            '#attributes' => [
              'class' => [
                'paragraph-type-label',
              ],
            ],
          ];
        }
      }
    }
  }

  // If there are no items, don't show the disabled region.
  if (!$has_non_layout_items) {
    unset($build['disabled']);
  }

  // Add a container so Javascript can respond to empty state.
  if (count($items) == 0) {
    $build['empty_container'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'erl-empty',
        ],
      ],
    ];
  }
  $build['#attached']['library'][] = 'entity_reference_layout/erl_widget';
  return $renderer
    ->render($build);
}

/**
 * Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'.
 */
function entity_reference_layout_form_field_ui_field_storage_add_form_alter(array &$form) {
  if (isset($form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['field_ui:entity_reference_layout_revisioned:paragraph'])) {

    // @todo Figure out why this option breaks the field config form
    // and reintroduce it if possible.
    // See https://www.drupal.org/project/entity_reference_layout/issues/3041126
    unset($form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['field_ui:entity_reference_layout_revisioned:paragraph']);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Indicate unsupported multilingual ERL field configuration.
 *
 * @see paragraphs_form_field_config_edit_form_alter
 */
function entity_reference_layout_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  /* @var \Drupal\field\Entity\FieldConfig $field */
  $field = $form_state
    ->getFormObject()
    ->getEntity();
  if (!\Drupal::hasService('content_translation.manager')) {
    return;
  }
  $bundle_is_translatable = \Drupal::service('content_translation.manager')
    ->isEnabled($field
    ->getTargetEntityTypeId(), $field
    ->getTargetBundle());
  if (!$bundle_is_translatable || $field
    ->getType() != 'entity_reference_layout_revisioned' || $field
    ->getSetting('target_type') != 'paragraph') {
    return;
  }

  // This is a translatable ERL field pointing to a paragraph.
  $message_display = 'warning';
  $message_text = t('Paragraphs fields do not support translation. See the <a href=":documentation">online documentation</a>.', [
    ':documentation' => Url::fromUri('https://www.drupal.org/node/2735121')
      ->toString(),
  ]);
  if ($form['translatable']['#default_value'] == TRUE) {
    $message_display = 'error';
  }
  $form['paragraphs_message'] = [
    '#type' => 'container',
    '#markup' => $message_text,
    '#attributes' => [
      'class' => [
        'messages messages--' . $message_display,
      ],
    ],
    '#weight' => 0,
  ];
}

/**
 * Implements hook_module_implements_alter().
 *
 * If "content_translation", move the form_alter implementation by the
 * entity_reference_layout at the end of the list, so that it might be
 * called after the content_translation one.
 * Otherwise the $form['translatable'] won't be defined in
 * entity_reference_layout_form_field_config_edit_form_alter.
 *
 * @see: https://www.hashbangcode.com/article/drupal-8-altering-hook-weights.
 */
function entity_reference_layout_module_implements_alter(&$implementations, $hook) {

  // Move our hook_entity_type_alter() implementation to the end of the list.
  if ($hook == 'form_alter' && isset($implementations['entity_reference_layout']) && isset($implementations['content_translation'])) {
    $hook_init = $implementations['entity_reference_layout'];
    unset($implementations['entity_reference_layout']);
    $implementations['entity_reference_layout'] = $hook_init;
  }
}

Functions

Namesort descending Description
entity_reference_layout_form_field_config_edit_form_alter Implements hook_form_FORM_ID_alter().
entity_reference_layout_form_field_ui_field_storage_add_form_alter Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'.
entity_reference_layout_help Implements hook_help().
entity_reference_layout_merge_attributes Merges $layout_options into an $attributes array.
entity_reference_layout_module_implements_alter Implements hook_module_implements_alter().
entity_reference_layout_theme Implements hook_theme().
entity_reference_layout_theme_suggestions_entity_reference_layout Implements hook_theme_suggestions().
theme_entity_reference_layout_radio Implements hook_entity_reference_layout_radio().
theme_entity_reference_layout_widget Themes the "ERL" field widget.
_erl_widget_sort_helper Helper function to sort array items by '_weight'.