Skip to content
ds.module 48.9 KiB
Newer Older
 * @file
Kristof De Jaeger's avatar
Kristof De Jaeger committed
 * Display Suite core functions.
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
Kristof De Jaeger's avatar
Kristof De Jaeger committed
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldConfigInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FormatterInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
Bram Goffings's avatar
Bram Goffings committed
use Drupal\Core\Url;
use Drupal\ds\Ds;
use Drupal\field\Entity\FieldConfig;
/**
 * Implements hook_help().
 */
function ds_help($route_name, RouteMatchInterface $route_match) {
  if ($route_name == 'help.page.ds') {
    $output = '<h3>' . t('About') . '</h3>';
    $output .= '<br/>' . t('The <a href=":link">Display Suite</a> module allows you to take full control over how your content is displayed using a drag and drop interface. Arrange your nodes, views, comments, user data etc. the way you want without having to work your way through dozens of template files.', [':link' => 'https://www.drupal.org/project/ds']);
    $output .= '<br/>' . t('It allows you to apply theme templates to entity type displays. It comes with predefined layouts such as "two columns", "three columns stacked", "three columns fluid" et cetera, but also lets you define your own.');
    $output .= '<br/>' . t('Display Suite allows you to create fields from tokens or blocks. This gives you full control over the way content is displayed without having to maintain dozens of twig files.');
    $output .= '<br/>' . t('More documentation about Display Suite in Drupal 8 can be found in Drupal\'s <a href=":documentation">Community Documentation </a>.', [':documentation' => 'https://www.drupal.org/node/2718943']);
    return $output;
/**
 * Implements hook_theme().
 */
function ds_theme() {
  $theme_functions['ds_entity_view'] = [
    'render element' => 'content',
  ];

  // Field templates.
  if (\Drupal::config('ds.settings')->get('field_template')) {
    $use_bc_templates = \Drupal::config('ds.settings')->get('ft_bc');
    $field_layouts = \Drupal::service('plugin.manager.ds.field.layout')->getDefinitions();
    foreach ($field_layouts as $key => $plugin) {
      if ($key != 'default') {
        $path = \Drupal::service('extension.list.module')->getPath($plugin['provider']) . '/templates';
        if ($plugin['provider'] == 'ds' && $use_bc_templates) {
          $path .= '/bc';
        }
        $theme_functions['field__' . $plugin['theme']] = [
          'render element' => 'elements',
          'template' => strtr($plugin['theme'], '_', '-'),
          'base hook' => 'field',

        if (!empty($plugin['path'])) {
          $theme_functions['field__' . $plugin['theme']]['file'] = $plugin['path'];
        }
      }
    }
  }

  return $theme_functions;
Kristof De Jaeger's avatar
Kristof De Jaeger committed
 * Implements hook_theme_registry_alter().
Kristof De Jaeger's avatar
Kristof De Jaeger committed
function ds_theme_registry_alter(&$theme_registry) {
  $layouts = Ds::getLayouts();
  /** @var \Drupal\Core\Layout\LayoutDefinition $info */
  foreach ($layouts as $info) {
    if (is_a($info->getClass(), DsLayout::class, TRUE)) {
      $layout_theme_hooks[$info->getThemeHook()] = 'layout';
    }
  }

  // Only add preprocess functions if entity exposes theme function, and this
  // layout is using the Display Suite layout class.
  foreach ($theme_registry as $theme_hook => $info) {
    if (array_key_exists($theme_hook, $layout_theme_hooks) || (!empty($info['base hook']) && array_key_exists($info['base hook'], $layout_theme_hooks))) {
      $theme_registry[$theme_hook]['preprocess functions'][] = 'ds_preprocess_ds_layout';
  // Check preprocess functions for ds fields.
  $field_theme_hooks = [];
  if (\Drupal::config('ds.settings')->get('field_template')) {
    $field_layouts = \Drupal::service('plugin.manager.ds.field.layout')
      ->getDefinitions();
    foreach ($field_layouts as $key => $plugin) {
      if ($key != 'default') {

        // Get the hook name.
        $hook_field_name = 'field__' . $plugin['theme'];

        if (isset($theme_registry[$hook_field_name])) {

          // Store for suggestions later.
          $field_theme_hooks[$hook_field_name] = 'field';

          // Variable holding the preprocess functions to add later.
          $preprocess_functions_to_add = [];

          // Overrides don't get the template_preprocess_field function.
          if (!in_array('template_preprocess_field', $theme_registry[$hook_field_name]['preprocess functions'])) {
            $preprocess_functions_to_add[] = 'template_preprocess_field';
          }

          // Overrides don't get the ds_preprocess_field function.
          if (!in_array('ds_preprocess_field', $theme_registry[$hook_field_name]['preprocess functions'])) {
            $preprocess_functions_to_add[] = 'ds_preprocess_field';
          }

          // We need to make sure the function runs asap as the dedicated field
          // preprocess function might need ds information, e.g.
          // template_preprocess_field__ds_field_expert().
          // template_preprocess() will be available, so put them right after.
          if (!empty($preprocess_functions_to_add)) {
            $preprocess_functions = $theme_registry[$hook_field_name]['preprocess functions'];
            array_splice($preprocess_functions, 1, 0, $preprocess_functions_to_add);
            $theme_registry[$hook_field_name]['preprocess functions'] = $preprocess_functions;
          }
        }
      }
    }
  }

  // ------------------------------------------------------------------------
  // Workaround to get theme suggestions working for templates using the
  // Display Suite class. It's borderline insane, but gets the job done.
  //
  // Note that this currently only works for Twig, but I assume, there isn't
  // any other engine out there yet for Drupal 8.
  //
  // Code based on drupal_find_theme_templates().
  //
  // @see
  //   - https://www.drupal.org/node/2862683 (core queue)
  //   - https://www.drupal.org/node/2802429 (DS queue)
  //      (and maybe others)
  // ------------------------------------------------------------------------

  // Merge layout and field hooks.
  $all_ds_theme_hooks = $layout_theme_hooks + $field_theme_hooks;

  $engine = \Drupal::theme()->getActiveTheme()->getEngine();
  if ($engine == 'twig') {

    $extension = '.html.twig';
    $theme_path = \Drupal::theme()->getActiveTheme()->getPath();

    // Escape the periods in the extension.
    $regex = '/' . str_replace('.', '\.', $extension) . '$/';
    // Get a listing of all template files in the path to search.
    $files = \Drupal::service('file_system')->scanDirectory($theme_path, $regex, ['key' => 'filename']);
    $patterns = array_keys($files);
    $implementations = [];

    foreach ($all_ds_theme_hooks as $hook => $base_hook) {

      // Ignored if not registered (which would be weird).
      if (!isset($theme_registry[$hook])) {
        continue;
      }

      $pattern = $info['pattern'] ?? ($hook . '__');
      if (!empty($pattern)) {
        // Transform _ in pattern to - to match file naming scheme
        // for the purposes of searching.
        $pattern = strtr($pattern, '_', '-');

        $matches = preg_grep('/^' . $pattern . '/', $patterns);
        if ($matches) {
          foreach ($matches as $match) {
            $file = $match;
            // Remove the extension from the filename.
            $file = str_replace($extension, '', $file);
            // Put the underscores back in for the hook name and register this
            // pattern.
            $info = $theme_registry[$hook];
            $arg_name = isset($info['variables']) ? 'variables' : 'render element';
            $new_hook = strtr($file, '-', '_');
            $implementations[$new_hook] = [
              'template' => $file,
              'path' => dirname($files[$match]->uri),
              $arg_name => $info[$arg_name],
              'type' => 'theme_engine',
              'theme path' => $theme_path,
            ];
            if (isset($theme_registry[$hook]['preprocess functions'])) {
              $implementations[$new_hook]['preprocess functions'] = $theme_registry[$hook]['preprocess functions'];
            }
          }
        }
      }
    }

    if (!empty($implementations)) {
      $theme_registry += $implementations;
    }
  }

  // ------------------------------------------------------------------------
  // End of workaround, hopefully we can kill this one day.
  // ------------------------------------------------------------------------

  // Run field group preprocess before ds_entity_view.
  if (function_exists('field_group_build_entity_groups')) {
    array_unshift($theme_registry['ds_entity_view']['preprocess functions'], 'field_group_build_entity_groups');
  }

  // Remove ds_preprocess_field in case field templates is not enabled.
  if (!\Drupal::config('ds.settings')->get('field_template')) {
    $key = array_search('ds_preprocess_field', $theme_registry['field']['preprocess functions']);
    if (!empty($key)) {
      unset($theme_registry['field']['preprocess functions'][$key]);
    }
  }
 * Implements hook_form_alter().
function ds_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) {
  $form_state->loadInclude('ds', 'inc', 'includes/field_ui');
  // Also load admin on behalf of DS extras when enabled.
Bram Goffings's avatar
Bram Goffings committed
  if (\Drupal::moduleHandler()->moduleExists('ds_extras')) {
    $form_state->loadInclude('ds_extras', 'inc', 'includes/admin');
Kristof De Jaeger's avatar
Kristof De Jaeger committed
  ds_field_ui_fields_layouts($form, $form_state);
/**
 * Implements hook_module_implements_alter().
 */
function ds_module_implements_alter(&$implementations, $hook) {
  // node_entity_view_display_alter() disables all labels on all fields
  // when the view mode is 'search_index'. If you set display modes for
  // this view mode by hand, then the hook isn't needed. Since this
  // may be called dozens of times on some pages, it's worth disabling it.
  if ($hook == 'entity_view_display_alter') {
    unset($implementations['node']);
  }
 * Implements hook_entity_view_alter().
function ds_entity_view_alter(&$build, EntityInterface $entity, EntityDisplayInterface $display) {
Kristof De Jaeger's avatar
Kristof De Jaeger committed
  static $field_permissions = FALSE;
  static $loaded = FALSE;

  $entity_type = $entity->getEntityTypeId();
  $bundle = $entity->bundle();
  $view_mode = $display->getMode();
  // Add extra metadata needed for contextual links.
  if (isset($build['#contextual_links'][$entity_type])) {
    $build['#contextual_links'][$entity_type]['metadata']['ds_bundle'] = $bundle;
    $build['#contextual_links'][$entity_type]['metadata']['ds_view_mode'] = $view_mode;
Kristof De Jaeger's avatar
Kristof De Jaeger committed
  }
  // If no layout is configured, stop executing.
  if (!$display->getThirdPartySetting('ds', 'layout')) {
    return;
  }

  // If Display Suite is disabled, stop here.
  if (Ds::isDisabled()) {
  // Load field permissions and layouts only once.
  if (!$loaded) {
    $loaded = TRUE;
    $field_permissions = \Drupal::config('ds_extras.settings')->get('field_permissions');
  // Get configuration.
  $configuration = $display->getThirdPartySettings('ds');
  // Don't fatal on missing layout plugins.
  $layout_id = $configuration['layout']['id'] ?? '';
  // Put #entity_type, #bundle and #layout on the build so we can access it in
  // ds_entity_variables().
  $build['#entity_type'] = $entity_type;
  $build['#bundle'] = $bundle;
  $build['#ds_configuration'] = $configuration;
  $build['#entity'] = $entity;
  // Implement UI limit.
  $components = $display->getComponents();
  foreach ($components as $field => $component) {
    if (isset($component['third_party_settings']['ds']) && !empty($component['third_party_settings']['ds']['ds_limit'])) {
      $limit = $component['third_party_settings']['ds']['ds_limit'];
      if (isset($build[$field]) && isset($build[$field]['#items'])) {
        if ($limit === 'delta' && isset($build['#ds_delta']) && isset($build['#ds_delta'][$field])) {

          // Get delta.
          $delta = $build['#ds_delta'][$field];

          // Remove caching for this entity as it otherwise won't work.
          unset($build['#cache']);

          $filtered_elements = Element::children($build[$field]);
          foreach ($filtered_elements as $filtered_element) {
            if ($filtered_element != $delta) {
              unset($build[$field][$filtered_element]);
        elseif (is_numeric($limit)) {

          // Remove caching for this entity as it otherwise won't work.
          unset($build['#cache']);

          $filtered_elements = Element::children($build[$field]);
          $filtered_elements = array_slice($filtered_elements, $limit);
          foreach ($filtered_elements as $filtered_element) {
            unset($build[$field][$filtered_element]);
Kristof De Jaeger's avatar
Kristof De Jaeger committed
  // Add Display Suite fields.
  $fields = Ds::getFields($entity_type);
  $field_values = !empty($configuration['fields']) ? $configuration['fields'] : [];
    foreach ($region as $weight => $key) {
      // Ignore if this field is not a DS field, just pull it in from the
      // entity.
      $field = $fields[$key];
      if (isset($field_values[$key]['formatter'])) {
        $field['formatter'] = $field_values[$key]['formatter'];
      }
      if (isset($field_values[$key]['settings'])) {
        $field['settings'] = $field_values[$key]['settings'];
      }
      $field_instance = Ds::getFieldInstance($key, $field, $entity, $view_mode, $display, $build);
      $field_value = $field_instance->build();
      $field_title = $field_instance->getTitle();
      // If the field value is cache data then we presume the value was empty
      // and we just have cache data as to why it's empty.
      if ($field_value instanceof CacheableMetadata) {
        CacheableMetadata::createFromRenderArray($build)
          ->merge($field_value)
          ->applyTo($build);
      }
      // Only allow non empty fields.
          '#theme' => 'field',
          '#field_type' => 'ds',
          '#weight' => $field_values[$key]['weight'] ?? $weight,
          '#label_display' => $field_values[$key]['label'] ?? 'inline',
          '#field_name' => $key,
          '#bundle' => $bundle,
          '#object' => $entity,
          '#entity_type' => $entity_type,
          '#view_mode' => '_custom',
          '#ds_view_mode' => $view_mode,
          '#items' => [(object) ['_attributes' => []]],
          '#is_multiple' => $field_instance->isMultiple(),
          '#access' => !($field_permissions && function_exists('ds_extras_ds_field_access')) || ds_extras_ds_field_access($key, $entity_type),

        if ($field_instance->isMultiple()) {
          $build[$key] += $field_value;
        }
        else {
          $build[$key][0] = [$field_value];
  // Defer to ds_entity_view theme hook to actually render the layout.
  $build['#theme'] = 'ds_entity_view';
}

/**
 * Process entity view.
 */
function template_preprocess_ds_entity_view(&$variables) {
  $build = $variables['content'];
  unset($build['#pre_render']);
  unset($build['#sorted']);
  unset($build['#children']);
  unset($build['#render_children']);
  unset($build['#prefix']);
  unset($build['#suffix']);
  unset($build['#attached']['html_head_link']);

  // Create region variables based on the layout settings.
  $use_field_names = \Drupal::config('ds.settings')->get('use_field_names');
  foreach (array_keys($configuration['regions']) as $region_name) {

    // Create the region content.
    if (!empty($configuration['regions'][$region_name])) {
      foreach ($configuration['regions'][$region_name] as $key => $field) {
        // Make sure the field exists.
        if (!isset($build[$field])) {
          continue;
        }

        // Always set weight.
        $build[$field]['#weight'] = $key;
          $regions[$region_name][$field] = $build[$field];
          $regions[$region_name][$key] = $build[$field];
  /* @var \Drupal\Core\Layout\LayoutInterface $layout */
  $layout = \Drupal::service('plugin.manager.core.layout')->createInstance($configuration['layout']['id'], $configuration['layout']['settings']);
  $build = array_merge($build, $layout->build($regions));
  // Disable CSS files when needed.
  if ($build['#ds_configuration']['layout']['disable_css'] ?? FALSE) {
    $library = $build['#ds_configuration']['layout']['library'];
    $attached = $build['#attached']['library'];

    $index = array_search($library, $attached);
    unset($build['#attached']['library'][$index]);
  }
 * Process layout.
 * This function is added in ds_theme_registry_alter().
  $layout_settings = $variables['settings'];
  // Fetch the entity type.
  $entity_type_id = NULL;
  if (isset($variables['content']['#entity_type'])) {
    $entity_type_id = $variables['content']['#entity_type'];
  }
  if (isset($variables['content']['#bundle'])) {
    $bundle = $variables['content']['#bundle'];
  // Template layout.
  if (!isset($variables['attributes']['class'])) {
    $variables['attributes']['class'] = [];
  // If the layout has wrapper class lets add it.
  if (!empty($layout_settings['classes']['layout_class'])) {
    foreach ($layout_settings['classes']['layout_class'] as $layout_class) {
      $variables['attributes']['class'][] = $layout_class;
    }
  }
  // Create region variables based on the layout settings.
  if (!empty($layout_settings['wrappers']) && is_array($layout_settings['wrappers'])) {
    foreach ($layout_settings['wrappers'] as $region_name => $wrapper) {
      // The new way of doing stuff is creating an attributes object.
      $classes = !empty($layout_settings['classes'][$region_name]) ? $layout_settings['classes'][$region_name] : [];
      if (!empty($classes)) {
        $variables[$region_name . '_attributes'] = new Attribute(['class' => $classes]);
      }
      else {
        $variables[$region_name . '_attributes'] = new Attribute();
      }

      // Merge layout builder's region attributes on the manage display page.
      if (!empty($variables['region_attributes'][$region_name])) {
        $variables[$region_name . '_attributes'] = new Attribute(array_merge($variables[$region_name . '_attributes']->toArray(), $variables['region_attributes'][$region_name]->toArray()));
      }

      $variables[$region_name . '_wrapper'] = !empty($layout_settings['wrappers'][$region_name]) ? $layout_settings['wrappers'][$region_name] : 'div';
  $variables['outer_wrapper'] = $layout_settings['outer_wrapper'] ?? 'div';

  // Add custom attributes if any.
  if (!empty($layout_settings['attributes'])) {
    $layout_attributes = explode(',', $layout_settings['attributes']);
    foreach ($layout_attributes as $layout_attribute) {
      $replaced_attribute = $layout_attribute;
      if (strpos($layout_attribute, '|') !== FALSE) {
        if (isset($entity_type_id) && isset($variables['content']['#entity_type'])) {
          $replaced_attribute = \Drupal::service('token')->replace(
            $layout_attribute,
            [$variables['content']['#entity_type'] => $variables['content']['#' . $entity_type_id]],
            ['clear' => TRUE]
        [$key, $attribute_value] = explode('|', $replaced_attribute);
        // Handle the class attribute as an array and others as strings.
        $key == 'class' ? $variables['attributes'][$key][] = $attribute_value : $variables['attributes'][$key] = $attribute_value;
  // Add an onclick attribute on the wrapper.
  if (!empty($layout_settings['link_attribute'])) {
    switch ($layout_settings['link_attribute']) {
      case 'content':
        if ($entity_type_id && $variables['content']['#' . $entity_type_id]->id() > 0) {
          $url = $variables['content']['#' . $entity_type_id]->toUrl();
      case 'custom':
        $url = $layout_settings['link_custom'];
        break;
          $url = \Drupal::service('token')->replace($layout_settings['link_custom'], [$entity_type_id => $variables['content']['#' . $entity_type_id]], ['clear' => TRUE]);
      if (is_string($url)) {
        $uri_parts = parse_url($url);
        if (empty($uri_parts['scheme'])) {
          $url = 'internal:/' . ltrim($url,'/');
        }
        $url = Url::fromUri($url);
      // Give the possibility to alter the url object.
      \Drupal::moduleHandler()->alter('ds_onclick_url', $url);
      $variables['attributes']['onclick'] = 'location.href=\'' . $url->toString() . '\'';
    }
  }

    if (isset($variables['content']['#ds_configuration'])) {
      $entity_classes = $variables['content']['#ds_configuration']['layout']['entity_classes'] ?? 'all_classes';
      if ($entity_classes != 'no_classes') {
        if ($entity_classes == 'all_classes') {
          $variables['attributes']['class'][] = Html::cleanCssIdentifier($entity_type_id);
          $variables['attributes']['class'][] = Html::cleanCssIdentifier($entity_type_id) . '--type-' . Html::cleanCssIdentifier($bundle);
          $variables['attributes']['class'][] = Html::cleanCssIdentifier($entity_type_id) . '--view-mode-' . Html::cleanCssIdentifier($variables['content']['#view_mode']);
        }
        elseif ($entity_classes == 'old_view_mode') {
          // Add (old style, uncleaned) view-mode-{name} to classes.
          if (!in_array('view-mode-' . $variables['content']['#view_mode'], $variables['attributes']['class'])) {
            $variables['attributes']['class'][] = 'view-mode-' . $variables['content']['#view_mode'];
          }
        }
      if ($entity_type_id == 'node' && function_exists('rdf_preprocess_node')) {
        $entity = $variables[$entity_type_id] ?? (isset($variables['content']['#' . $entity_type_id]) ? $variables['content']['#' . $entity_type_id] : '');
        if ($entity) {
          $variables['node'] = $entity;
          // We disable the date feature for now as it throws notices.
          $variables['display_submitted'] = FALSE;
          rdf_preprocess_node($variables);
        }
      }

      // Let other modules know we have rendered.
      // Let other modules alter the ds array before rendering.
        'entity_type' => $variables['content']['#entity_type'],
        'bundle' => $variables['content']['#bundle'],
        'view_mode' => $variables['content']['#view_mode'],
      // Special case for User entity, because $variables['user'] contains the
      // currently logged in user.
      if($entity_type_id !== 'user' && isset($variables[$entity_type_id])) {
        $context['entity'] = $variables[$entity_type_id];
      }
      elseif (isset($variables['content']['#' . $entity_type_id])) {
        $context['entity'] = $variables['content']['#' . $entity_type_id];
      }

      \Drupal::moduleHandler()->alter('ds_pre_render', $variables['content'], $context, $variables);

  // Copy the regions from 'content' into the top-level.
  foreach (Element::children($variables['content']) as $name) {
    $variables[$name] = $variables['content'][$name];
  }
  // Copy entity to top level to improve theme experience.
  if (isset($variables['content']['#entity']) && isset($variables['content']['#entity_type'])) {
    $variables[$variables['content']['#entity_type']] = $variables['content']['#entity'];
  }
 * Implements hook_theme_suggestions_alter().
 */
function ds_theme_suggestions_alter(&$suggestions, $variables, $base_theme_hook) {
  if (isset($variables['content']) && is_array($variables['content']) && !empty($variables['content']['#ds_configuration']['layout']['id']) && $base_theme_hook != 'ds_entity_view') {
    $entity_id = $variables['content']['#' . $variables['content']['#entity_type']]->id();
    $layout_hook = $variables['content']['#ds_configuration']['layout']['id'];
    if (!\Drupal::config('ds.settings')->get('layout_suggestion_bc')) {
      $layout = \Drupal::service('plugin.manager.core.layout')->getDefinition($layout_hook);
      if (isset($layout) && ($theme_hook = $layout->getThemeHook())) {
        $layout_hook = $theme_hook;
      }
    }
    $suggestions[] = $layout_hook . '__' . $variables['content']['#entity_type'];
    $suggestions[] = $layout_hook . '__' . $variables['content']['#entity_type'] . '_' . $variables['content']['#view_mode'];
    $suggestions[] = $layout_hook . '__' . $variables['content']['#entity_type'] . '_' . $variables['content']['#bundle'];
    $suggestions[] = $layout_hook . '__' . $variables['content']['#entity_type'] . '_' . $variables['content']['#bundle'] . '_' . $variables['content']['#view_mode'];
    $suggestions[] = $layout_hook . '__' . $variables['content']['#entity_type'] . '__' . $entity_id;
/**
 * Implements hook_contextual_links_view_alter().
 */
function ds_contextual_links_view_alter(&$element, $items) {
  $def = \Drupal::service('entity_type.manager')->getDefinitions();
  $entity_type = array_keys($element['#contextual_links'])[0];

  if (isset($def[$entity_type]) && $def[$entity_type]->get('field_ui_base_route')) {
    if (!empty($entity_type) && \Drupal::moduleHandler()->moduleExists('field_ui') && \Drupal::currentUser()->hasPermission('administer node display')) {

      // This might not exist (especially in panels environments).
      if (!isset($element['#contextual_links'][$entity_type]['metadata']['ds_bundle'])) {
        return;
      }

      $bundle = $element['#contextual_links'][$entity_type]['metadata']['ds_bundle'];
      $view_mode = $element['#contextual_links'][$entity_type]['metadata']['ds_view_mode'];
      $route_name = "entity.entity_view_display.$entity_type.view_mode";
      $type = $def[$entity_type]->getBundleEntityType();
Bram Goffings's avatar
Bram Goffings committed
        $type => $bundle,
        'view_mode_name' => $view_mode,
Bram Goffings's avatar
Bram Goffings committed

      $url = new Url($route_name, $route_params);

      $destination = \Drupal::destination()->getAsArray();
      $url->setOption('query', $destination);
      // When there is no bundle defined, return.
      if (!empty($bundle)) {
        $element['#links']['manage-display'] = [
          'title' => t('Manage display'),
Bram Goffings's avatar
Bram Goffings committed
          'url' => $url,
 * Implements hook_local_tasks_alter().
 */
function ds_local_tasks_alter(&$local_tasks) {
  if (!\Drupal::moduleHandler()->moduleExists('contextual') || !\Drupal::moduleHandler()->moduleExists('field_ui')) {
    unset($local_tasks['ds.manage_node_display']);
    unset($local_tasks['ds.manage_user_display']);
    unset($local_tasks['ds.manage_taxonomy_term_display']);
  }

/**
 * Implements hook_preprocess_field().
 */
function ds_preprocess_field(&$variables) {
Kristof De Jaeger's avatar
Kristof De Jaeger committed
  $entity_type = $variables['element']['#entity_type'];
  $bundle = $variables['element']['#bundle'];
  $view_mode = $variables['element']['#ds_view_mode'] ?? $variables['element']['#view_mode'];
  /* @var $entity_display EntityDisplayInterface */
Kristof De Jaeger's avatar
Kristof De Jaeger committed
  $entity_display = Ds::getDisplay($entity_type, $bundle, $view_mode);
  // Check if this field is being rendered as a layout builder FieldBlock
  // @see Drupal\layout_builder\Plugin\Block\FieldBlock::build();
  $is_layout_builder = (!empty($variables['element']['#third_party_settings']['layout_builder']['view_mode']) && \Drupal::config('ds.settings')->get('ft_layout_builder'));
  if (($entity_display && $entity_display->getThirdPartySetting('ds', 'layout') || $is_layout_builder)) {
    // Get the field name and field instance info - if available.
    $field_name = $variables['element']['#field_name'];

    if ($is_layout_builder && !empty($variables['element']['#third_party_settings']['ds']['ft'])) {
      $field_settings = [];
      $field_settings[$entity_type][$bundle][$view_mode][$field_name]['ft'] = $variables['element']['#third_party_settings']['ds']['ft'];
    }
    else {
      static $field_settings = [];
      if (!isset($field_settings[$entity_type][$bundle][$view_mode])) {

        $f = [];

        // Get third party settings for Core fields.
        foreach ($entity_display->getComponents() as $key => $info) {
          if (!empty($info['third_party_settings']['ds']['ft'])) {
            $f[$key]['ft'] = $info['third_party_settings']['ds']['ft'];
          }
        // Get third party settings for Display Suite fields.
        $ds_fields_third_party_settings = $entity_display->getThirdPartySetting('ds', 'fields');
        if ($ds_fields_third_party_settings) {
          $f += $entity_display->getThirdPartySetting('ds', 'fields');
        }
        $field_settings[$entity_type][$bundle][$view_mode] = $f;
      }
    // Check if this field has custom output settings.
    if (isset($field_settings[$entity_type][$bundle][$view_mode][$field_name]['ft'])) {
      $config = $field_settings[$entity_type][$bundle][$view_mode][$field_name]['ft'];
      $variables['ds-config'] = $config;
      // When dealing with a field template we need to massage to values before
      // printing to prevent layout issues.
Bram Goffings's avatar
Bram Goffings committed
      if (isset($config['id']) && $config['id'] != 'default' && !empty($variables['ds-config']['settings'])) {
        /* @var \Drupal\ds\Plugin\DsFieldTemplate\DsFieldTemplateInterface $layout_instance */
        $layout_instance = \Drupal::service('plugin.manager.ds.field.layout')->createInstance($config['id']);
        if (isset($variables['element']['#object'])) {
          $layout_instance->setEntity($variables['element']['#object']);
        }
        $layout_instance->massageRenderValues($variables['ds-config']['settings'], $config['settings']);
    if (isset($config['settings']['classes'])) {
      foreach ($config['settings']['classes'] as $class_name) {
        if (isset($variables['element']['#object'])) {
          $class_name = \Drupal::token()->replace(
            $class_name,
            [$entity_type => $variables['element']['#object']],
            ['clear' => TRUE]
        $variables['attributes']['class'][] = $class_name;
      }
    // Alter the label if configured.
    if (!$variables['label_hidden']) {
      if (!empty($config['settings']['lb'])) {
        $variables['label'] = t(Html::escape($config['settings']['lb']));
 * Implements hook_theme_suggestions_HOOK_alter().
 *
 * The suggestion alters for field templates.
 */
function ds_theme_suggestions_field_alter(&$suggestions, $variables) {
Kristof De Jaeger's avatar
Kristof De Jaeger committed
  $entity_type = $variables['element']['#entity_type'];
  $bundle = $variables['element']['#bundle'];
  $view_mode = $variables['element']['#ds_view_mode'] ?? $variables['element']['#view_mode'];
  /* @var $entity_display EntityDisplayInterface */
Kristof De Jaeger's avatar
Kristof De Jaeger committed
  $entity_display = Ds::getDisplay($entity_type, $bundle, $view_mode);
  // Check if this field is being rendered as a layout builder FieldBlock and
  // that layout builder field templates are enabled
  // @see Drupal\layout_builder\Plugin\Block\FieldBlock::build();
  $is_layout_builder = (!empty($variables['element']['#third_party_settings']['layout_builder']['view_mode']) && \Drupal::config('ds.settings')->get('ft_layout_builder'));
  if ($entity_display && ($entity_display->getThirdPartySetting('ds', 'layout') || $is_layout_builder)) {
    // Get the field name and field instance info - if available.
    $field_name = $variables['element']['#field_name'];

    $field_theme_function = \Drupal::config('ds.settings')->get('ft_default');
    $undo_default_field_template_fix = \Drupal::config('ds.settings')->get('ft_default_bc');
    if ($is_layout_builder && !empty($variables['element']['#third_party_settings']['ds']['ft'])) {
      $field_settings[$entity_type][$bundle][$view_mode][$field_name]['ft'] = $variables['element']['#third_party_settings']['ds']['ft'];
    }
    else {
      static $field_settings = [];
      if (!isset($field_settings[$entity_type][$bundle][$view_mode])) {
        $f = [];

        // Get third party settings for Core fields.
        foreach ($entity_display->getComponents() as $key => $info) {
          if (!empty($info['third_party_settings']['ds']['ft'])) {
            $f[$key]['ft'] = $info['third_party_settings']['ds']['ft'];
          }
        }
        // Get third party settings for Display Suite fields.
        $ds_fields_third_party_settings = $entity_display->getThirdPartySetting('ds', 'fields');
        if ($ds_fields_third_party_settings) {
          $f += $entity_display->getThirdPartySetting('ds', 'fields');
        $field_settings[$entity_type][$bundle][$view_mode] = $f;
    // Check if this field has custom output settings.
    if (isset($field_settings[$entity_type][$bundle][$view_mode][$field_name]['ft'])) {
      $config = $field_settings[$entity_type][$bundle][$view_mode][$field_name]['ft'];
    // Initialize suggestion name.
    $suggestion = '';
    // Determine the field template. In case it's something different.
    if (isset($config['id']) && $config['id'] != 'default') {
      $layout_instance = \Drupal::service('plugin.manager.ds.field.layout')->createInstance($config['id']);
      // Either it uses the function.
      $suggestions[] = 'field__' . $layout_instance->getThemeFunction();
      // Or the template file(s).
      $suggestion = 'field__' . $config['id'];
      // Either it uses the function.
      if ($undo_default_field_template_fix) {
        $suggestions[] = 'field__theme_ds_field_' . $field_theme_function;
      }
      else {
        $suggestions[] = 'field__ds_field_' . $field_theme_function;
      }
      // Or the template file(s).
      $set_suggestion = TRUE;
      if ($field_theme_function == 'default' && !$undo_default_field_template_fix) {
        $set_suggestion = FALSE;
      }

      if ($set_suggestion) {
        $suggestion = 'field__' . $field_theme_function;
      }
    if (!empty($suggestion)) {
      $suggestions[] = $suggestion;
      $suggestions[] = $suggestion . '__' . $field_name;
      $suggestions[] = $suggestion . '__' . $variables['element']['#bundle'];
      $suggestions[] = $suggestion . '__' . $variables['element']['#bundle'] . '__' . $variables['element']['#view_mode'];
      $suggestions[] = $suggestion . '__' . $field_name . '__' . $variables['element']['#bundle'];
      $suggestions[] = $suggestion . '__' . $field_name . '__' . $variables['element']['#bundle'] . '__' . $variables['element']['#view_mode'];
      $suggestions[] = $suggestion . '__' . $variables['element']['#entity_type'] . '__' . $field_name;
      $suggestions[] = $suggestion . '__' . $variables['element']['#entity_type'] . '__' . $variables['element']['#bundle'];
      $suggestions[] = $suggestion . '__' . $variables['element']['#entity_type'] . '__' . $variables['element']['#bundle'] . '__' . $variables['element']['#view_mode'];
      $suggestions[] = $suggestion . '__' . $variables['element']['#entity_type'] . '__' . $field_name . '__' . $variables['element']['#bundle'];
      $suggestions[] = $suggestion . '__' . $variables['element']['#entity_type'] . '__' . $field_name . '__' . $variables['element']['#bundle'] . '__' . $variables['element']['#view_mode'];

    // Custom DS fields name may contain colon separators or dashes; replace it
    // with "__" or "_" to ensure suggestions are compatible with file names on
    // all systems.
    foreach ($suggestions as $key => $suggestion) {
      $suggestions[$key] = str_replace([':', '-'], ['__', '_'], $suggestion);
    }
 * Field template settings form.
Bram Goffings's avatar
Bram Goffings committed
function ds_field_template_settings_form(array &$form, FormStateInterface &$form_state, array $context) {
  $functions = Ds::getFieldLayoutOptions();
  $default_field_function = \Drupal::config('ds.settings')->get('ft_default');
  if (empty($default_field_function)) {
    $default_field_function = 'default';

  if (is_array($context['instance'])) {
    $key = $context['instance']['field_name'];
  }
  else {
    $key = $context['instance']->getName();
  // Check if this is the layout builder field block form
  $is_layout_builder = ($form_state->getBuildInfo()['base_form_id'] == 'layout_builder_configure_block');

  // Field (plugin) setting. If it's an array, this is a DS field, otherwise,
  // it's a core base or field api field.
  if (is_array($context['instance'])) {
    $plugin_settings = $form_state->get('plugin_settings');
    $field_settings = $plugin_settings[$key]['ft'] ?? [];
  else {
    $field_settings = $context['formatter']->getThirdPartySetting('ds', 'ft');
  }
  // In case with an ajax refresh we fetch the function from a different place.
Bram Goffings's avatar
Bram Goffings committed
  $values = $form_state->getValues();
  if (isset($values['fields'][$key]['settings_edit_form']['settings']['ft']['id'])) {
    $field_function = $values['fields'][$key]['settings_edit_form']['settings']['ft']['id'];
  elseif (isset($values['fields'][$key]['settings_edit_form']['third_party_settings']['ds']['ft']['id'])) {
    $field_function = $values['fields'][$key]['settings_edit_form']['third_party_settings']['ds']['ft']['id'];
  }
  elseif (isset($values['settings']['formatter']['third_party_settings']['ds']['ft']['id'])) {
    $field_function = $values['settings']['formatter']['third_party_settings']['ds']['ft']['id'];
  }
    $field_function = $field_settings['id'] ?? $default_field_function;
Bram Goffings's avatar
Bram Goffings committed
  if (!isset($functions[$field_function])) {
    $field_function = $default_field_function;
  }

    '#title' => t('Choose a Field Template'),
    '#type' => 'select',
    '#options' => $functions,
    '#default_value' => $field_function,
    '#submit' => ['ds_field_ui_display_overview_multistep_submit'],
  ];

  // Support layout builder field blocks
  if ($is_layout_builder) {
    $form['ft']['id']['#ajax'] = [
      'callback' => 'ds_layout_builder_multistep_js',
      'wrapper' => 'formatter-settings-wrapper',
    ];
  }
  else {
    $form['ft']['id']['#field_name'] = $key;
      'callback' => 'ds_field_ui_display_overview_multistep_js',
      'wrapper' => 'field-display-overview-wrapper',