layout_paragraphs-1.0.x-dev/src/Element/LayoutParagraphsBuilder.php

src/Element/LayoutParagraphsBuilder.php
<?php

namespace Drupal\layout_paragraphs\Element;

use Drupal\Core\Url;
use Drupal\Core\Render\Markup;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Render\RendererInterface;
use Drupal\paragraphs\ParagraphInterface;
use Drupal\Core\Access\AccessResultAllowed;
use Drupal\layout_paragraphs\Utility\Dialog;
use Drupal\Core\Access\AccessResultForbidden;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Layout\LayoutPluginManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\layout_paragraphs\LayoutParagraphsSection;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\layout_paragraphs\LayoutParagraphsComponent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;

/**
 * Defines a render element for building the Layout Builder UI.
 *
 * @RenderElement("layout_paragraphs_builder")
 *
 * @internal
 *   Plugin classes are internal.
 */
class LayoutParagraphsBuilder extends RenderElementBase implements ContainerFactoryPluginInterface {

  /**
   * The layout paragraphs tempstore service.
   *
   * @var \Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository
   */
  protected $tempstore;

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The Layouts Manager.
   *
   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
   */
  protected $layoutPluginManager;

  /**
   * The Renderer service property.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The entity type bundle info service.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  protected $entityTypeBundleInfo;

  /**
   * Indicates whether the element is in translation mode.
   *
   * @var bool
   */
  protected $isTranslating;

  /**
   * The layout paragraphs layout object.
   *
   * @var \Drupal\layout_paragraphs\LayoutParagraphsLayout
   */
  protected $layoutParagraphsLayout;

  /**
   * The button labels configuration.
   *
   * @var array
   */
  protected $buttonLabels;

  /**
   * {@inheritdoc}
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    LayoutParagraphsLayoutTempstoreRepository $tempstore_repository,
    EntityTypeManagerInterface $entity_type_manager,
    LayoutPluginManagerInterface $layout_plugin_manager,
    RendererInterface $renderer,
    EntityTypeBundleInfoInterface $entity_type_bundle_info,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->tempstore = $tempstore_repository;
    $this->entityTypeManager = $entity_type_manager;
    $this->layoutPluginManager = $layout_plugin_manager;
    $this->renderer = $renderer;
    $this->entityTypeBundleInfo = $entity_type_bundle_info;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('layout_paragraphs.tempstore_repository'),
      $container->get('entity_type.manager'),
      $container->get('plugin.manager.core.layout'),
      $container->get('renderer'),
      $container->get('entity_type.bundle.info'),
    );
  }

  /**
   * {@inheritdoc}
   *
   * Properties:
   * - #layout_paragraphs_layout: a LayoutParagraphsLayout instance.
   * - #uuid: if provided, the uuid of the single paragraph to render.
   * - #is_translating: if translating content.
   */
  public function getInfo() {
    return [
      '#layout_paragraphs_layout' => NULL,
      '#uuid' => NULL,
      '#theme' => 'layout_paragraphs_builder',
      '#is_translating' => NULL,
      '#pre_render' => [
        [$this, 'preRender'],
      ],
    ];
  }

  /**
   * Pre-render callback: Renders the UI.
   *
   * @todo Better inline comments for all functionality in this method.
   */
  public function preRender($element) {
    $this->layoutParagraphsLayout = $this->tempstore->get($element['#layout_paragraphs_layout']);
    $this->isTranslating = $element['#is_translating'] ?? FALSE;
    $this->buttonLabels = $this->layoutParagraphsLayout->getSetting('button_labels');
    $element_uuid = $element['#uuid'];
    $preview_view_mode = $this->layoutParagraphsLayout->getSetting('preview_view_mode', 'default');

    $element['#layout_paragraphs_layout'] = $this->layoutParagraphsLayout;
    $element['#components'] = [];
    if ($this->isTranslating()) {
      $element['#translation_warning'] = $this->translationWarning();
    }
    // Build a flat list of component build arrays.
    foreach ($this->layoutParagraphsLayout->getComponents() as $component) {
      /** @var \Drupal\layout_paragraphs\LayoutParagraphsComponent $component */
      $element['#components'][$component->getEntity()->uuid()] = $this->buildComponent($component, $preview_view_mode);
    }

    // Nest child components inside their respective sections and regions.
    foreach ($this->layoutParagraphsLayout->getComponents() as $component) {
      /** @var \Drupal\layout_paragraphs\LayoutParagraphsComponent $component */
      $uuid = $component->getEntity()->uuid();
      if ($component->isLayout()) {
        $section = $this->layoutParagraphsLayout->getLayoutSection($component->getEntity());
        $layout_plugin_instance = $this->layoutPluginInstance($section);
        foreach (array_keys($element['#components'][$uuid]['regions']) as $region_name) {
          foreach ($section->getComponentsForRegion($region_name) as $child_component) {
            $child_uuid = $child_component->getEntity()->uuid();
            $element['#components'][$uuid]['regions'][$region_name][$child_uuid] =& $element['#components'][$child_uuid];
          }
        }
        $element['#components'][$uuid]['regions'] = $layout_plugin_instance->build($element['#components'][$uuid]['regions']);
        $element['#components'][$uuid]['regions']['#weight'] = 1000;
      }
    }

    // If an element #uuid is provided, render the matching element.
    // This is used in cases where a single component needs
    // to be rendered - for example, as part of an AJAX response.
    if ($element_uuid) {
      if (isset($element['#components'][$element_uuid])) {
        return [
          'build' => $element['#components'][$element_uuid],
        ];
      }
    }

    $element['#attributes'] = [
      'class' => [
        'lp-builder',
        'lp-builder-' . $this->layoutParagraphsLayout->id(),
      ],
      'data-lpb-ui-id' => $this->layoutParagraphsLayout->id(),
      'data-lpb-id' => $this->layoutParagraphsLayout->id(),
    ] + ($element['#attributes'] ?? []);
    $element['#attached']['library'] = ['layout_paragraphs/builder'];
    $element['#attached']['drupalSettings']['lpBuilder'][$this->layoutParagraphsLayout->id()] = $this->layoutParagraphsLayout->getSettings();
    $element['#is_empty'] = $this->layoutParagraphsLayout->isEmpty();
    $element['#empty_message'] = $this->layoutParagraphsLayout->getSetting('empty_message', $this->t('Start adding content.'));
    $element['#root_components'] = [];
    foreach ($this->layoutParagraphsLayout->getRootComponents() as $component) {
      /** @var \Drupal\layout_paragraphs\LayoutParagraphsComponent $component */
      $uuid = $component->getEntity()->uuid();
      $element['#root_components'][$uuid] =& $element['#components'][$uuid];
    }
    if (count($element['#root_components'])) {
      $element['#attributes']['class'][] = 'has-components';
    }
    else {
      $this->addJsUiElement(
        $element,
        $this->doRender($this->insertComponentButton(['layout_paragraphs_layout' => $this->layoutParagraphsLayout->id()], [], 0, ['center'])),
        'insert'
      );
    }
    return $element;
  }

  /**
   * Returns the build array for a single layout component.
   *
   * @param \Drupal\layout_paragraphs\LayoutParagraphsComponent $component
   *   The component to render.
   * @param string $preview_view_mode
   *   The view mode to use for rendering paragraphs.
   *
   * @return array
   *   The build array.
   */
  protected function buildComponent(LayoutParagraphsComponent $component, $preview_view_mode = 'default') {
    $entity = $component->getEntity();
    $entity->_layoutParagraphsBuilder = TRUE;
    $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId());
    $build = $view_builder->view($entity, $preview_view_mode, $entity->language()->getId());
    $build['#post_render'] = [
      [$this, 'postRenderComponent'],
    ];
    $build['#attributes']['data-uuid'] = $entity->uuid();
    $build['#attributes']['data-type'] = $entity->bundle();
    $build['#attributes']['data-lpb-ui-id'] = $entity->uuid() ?? 'new-' . $entity->bundle();
    $build['#attributes']['class'][] = 'js-lpb-component';
    $build['#layout_paragraphs_component'] = TRUE;
    if ($entity->isNew()) {
      $build['#attributes']['class'][] = 'is_new';
    }

    $url_params = [
      'layout_paragraphs_layout' => $this->layoutParagraphsLayout->id(),
    ];
    $query_params = [
      'sibling_uuid' => $entity->uuid(),
    ];
    if ($parent_uuid = $component->getParentUuid()) {
      $query_params['parent_uuid'] = $parent_uuid;
    }
    if ($region = $component->getRegion()) {
      $query_params['region'] = $region;
    }

    $controls = [
      '#theme' => 'layout_paragraphs_builder_controls',
      '#attributes' => [
        'class' => [
          'lpb-controls',
        ],
      ],
      '#uuid' => $entity->uuid(),
      '#layout_paragraphs_layout' => $this->layoutParagraphsLayout,
      '#edit_access' => $this->editAccess($entity),
      '#duplicate_access' => $this->duplicateAccess($entity) && $this->checkCardinality(),
      '#delete_access' => $this->deleteAccess($entity),
    ];
    $build['#attached']['drupalSettings']['lpBuilder']['uiElements'][$entity->uuid()] = [];
    $this->addJsUiElement($build, $this->doRender($controls), 'controls', 'prepend');

    if ($this->createAccess() && $this->checkCardinality()) {
      $this->addJsUiElement(
        $build,
        $this->doRender($this->insertComponentButton($url_params, $query_params + ['placement' => 'before'], -10000, ['before'])),
        'insert_before',
        'prepend'
      );
      $this->addJsUiElement(
        $build,
        $this->doRender($this->insertComponentButton($url_params, $query_params + ['placement' => 'after'], -10000, ['after'])),
        'insert_after',
        'append'
      );
    }

    if ($component->isLayout()) {
      $section = $this->layoutParagraphsLayout->getLayoutSection($entity);
      $layout_instance = $this->layoutPluginInstance($section);
      $region_names = $layout_instance->getPluginDefinition()->getRegionNames();

      $build['#attributes']['class'][] = 'lpb-layout';
      $build['#attributes']['data-layout'] = $section->getLayoutId();
      $build['#layout_plugin_instance'] = $layout_instance;
      $build['regions'] = [];
      foreach ($region_names as $region_name) {
        $url_params = [
          'layout_paragraphs_layout' => $this->layoutParagraphsLayout->id(),
        ];
        $query_params = [
          'parent_uuid' => $entity->uuid(),
          'region' => $region_name,
        ];
        $build['regions'][$region_name] = [
          '#attributes' => [
            'class' => [
              'js-lpb-region',
            ],
            'data-region' => $region_name,
            'data-region-uuid' => $entity->uuid() . '-' . $region_name,
            'data-lpb-ui-id' => $entity->uuid() . '-' . $region_name,
          ],
        ];
        if ($this->createAccess() && $this->checkCardinality()) {
          $this->addJsUiElement(
            $build['regions'][$region_name],
            $this->doRender($this->insertComponentButton($url_params, $query_params, 10000, ['center'])),
            'insert'
          );
        }
      }
    }
    return $build;
  }

  /**
   * Filters problematic markup from rendered component.
   *
   * @param mixed $content
   *   The rendered content.
   * @param array $element
   *   The render element array.
   *
   * @return mixed
   *   The filtered content.
   */
  public function postRenderComponent($content, array $element) {
    if (strpos($content, '<form') !== FALSE) {
      // Because the Layout Paragraphs Builder is often rendered within a form,
      // we need to strip out any form tags, "name" attributes, and "required"
      // attributes to prevent Drupal from attempting to process the form when
      // the parent entity is saved.
      // @see https://www.drupal.org/project/layout_paragraphs/issues/3263715
      // First, replace form tags with divs.
      $search = [
        '<form',
        '</form>',
      ];
      $replace = [
        '<div',
        '</div>',
      ];
      $content = str_replace($search, $replace, $content);
      // Strip out "name" attributes.
      $content = preg_replace('/(<[^>]+) name\s*=\s*".*?"/i', '$1', $content);
      // Strip out "required" attributes.
      $content = preg_replace('/(<[^>]+) required\s*=\s*".*?"/i', '$1', $content);
    }
    return $content;
  }

  /**
   * Returns the render array for a insert component button.
   *
   * @param array[] $route_params
   *   The route parameters for the link.
   * @param array[] $query_params
   *   The query parameters for the link.
   * @param int $weight
   *   The weight of the button element.
   * @param array[] $classes
   *   A list of classes to append to the container.
   *
   * @return array
   *   The render array.
   */
  protected function insertComponentButton(array $route_params = [], array $query_params = [], int $weight = 0, array $classes = []) {
    if ($this->buttonLabels['enable_tooltips'] ?? FALSE) {
      $classes[] = 'use-tooltip';
    }
    return [
      '#theme' => 'layout_paragraphs_insert_component_btn',
      '#title' => $this->insertComponentButtonTitle($query_params['parent_uuid'] ?? NULL),
      '#weight' => $weight,
      '#attributes' => [
        'class' => array_merge(['lpb-btn--add', 'use-ajax'], $classes),
        'data-dialog-type' => 'dialog',
        'data-dialog-options' => Json::encode([
          'target' => Dialog::dialogId($this->layoutParagraphsLayout),
          'modal' => TRUE,
          'drupalAutoButtons' => FALSE,
          'dialogClass' => 'lpb-dialog',
        ]),
      ],
      '#url' => Url::fromRoute('layout_paragraphs.builder.choose_component', $route_params, ['query' => $query_params]),
    ];
  }

  /**
   * Returns the title for the insert component button.
   *
   * @param string|null $parent_uuid
   *   The parent UUID, if available.
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup|string
   *   The title for the button.
   */
  protected function insertComponentButtonTitle(?string $parent_uuid = NULL) {
    $default_label = $this->buttonLabels['default'] ?? $this->t('Add Component');

    if (empty($parent_uuid)) {
      return $this->buttonLabels['root'] ?? $default_label;
    }
    else {
      // If a parent UUID is provided, get the parent component.
      $parent_component = $this->layoutParagraphsLayout->getComponentByUuid($parent_uuid);
      if ($parent_component) {
        $parent_entity = $parent_component->getEntity();
        $parent_type = $parent_entity->bundle();
        if (!empty($this->buttonLabels[$parent_type])) {
          return $this->buttonLabels[$parent_type];
        }
      }
      return $default_label;
    }
  }

  /**
   * Returns an array of dialog options.
   */
  protected function dialogOptions() {
    return [
      'modal' => TRUE,
      'target' => Dialog::dialogId($this->layoutParagraphsLayout),
    ];
  }

  /**
   * Builds a translation warning message.
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup|null
   *   The translation warning.
   */
  protected function translationWarning() {
    if ($this->isTranslating()) {
      if ($this->supportsAsymmetricTranslations()) {
        return $this->t('You are in translation mode. Changes will only affect the current language.');
      }
      else {
        return $this->t('You are in translation mode. You cannot add or remove items while translating. Reordering items will affect all languages.');
      }
    }
  }

  /**
   * Loads a layout plugin instance for a layout paragraph section.
   *
   * @param \Drupal\layout_paragraphs\LayoutParagraphsSection $section
   *   The section.
   */
  protected function layoutPluginInstance(LayoutParagraphsSection $section) {
    $layout_id = $section->getLayoutId();
    $layout_config = $section->getLayoutConfiguration();
    $layout_config['layout_paragraphs_section'] = $section;
    $layout_instance = $this->layoutPluginManager->createInstance($layout_id, $layout_config);
    return $layout_instance;
  }

  /**
   * Returns an AccessResult object.
   *
   * @param \Drupal\paragraphs\ParagraphInterface $paragraph
   *   The paragraph entity.
   *
   * @return bool
   *   True if user can edit.
   */
  protected function editAccess(ParagraphInterface $paragraph) {
    return $paragraph->access('update');
  }

  /**
   * Returns an AccessResult object.
   *
   * @param \Drupal\paragraphs\ParagraphInterface $paragraph
   *   The paragraph entity.
   *
   * @return bool
   *   True if user can edit.
   */
  protected function deleteAccess(ParagraphInterface $paragraph) {
    if ($this->isTranslating() && !($this->supportsAsymmetricTranslations())) {
      $access = new AccessResultForbidden('Cannot delete paragraphs while in translation mode.');
      return $access->isAllowed();
    }
    return $paragraph->access('delete');
  }

  /**
   * Returns an AccessResult object.
   *
   * @return bool
   *   True if user can create.
   */
  protected function createAccess() {
    $access = new AccessResultAllowed();
    if ($this->isTranslating() && !($this->supportsAsymmetricTranslations())) {
      $access = new AccessResultForbidden('Cannot add paragraphs while in translation mode.');
    }
    return $access->isAllowed();
  }

  /**
   * Returns an AccessResult object.
   *
   * @param \Drupal\paragraphs\ParagraphInterface $paragraph
   *   The paragraph entity.
   *
   * @return bool
   *   True if user can duplicate.
   */
  protected function duplicateAccess(ParagraphInterface $paragraph) {
    if ($this->isTranslating() && !($this->supportsAsymmetricTranslations())) {
      $access = new AccessResultForbidden('Cannot duplicate paragraphs while in translation mode.');
      return $access->isAllowed();
    }
    return $paragraph->access('create');
  }

  /**
   * Returns TRUE if in translation context.
   *
   * @return bool
   *   TRUE if translating.
   */
  protected function isTranslating() {
    return $this->isTranslating;
  }

  /**
   * Whether or not to support asymmetric translations.
   *
   * @see https://www.drupal.org/project/paragraphs/issues/2461695
   * @see https://www.drupal.org/project/paragraphs/issues/2904705
   * @see https://www.drupal.org/project/paragraphs_asymmetric_translation_widgets
   *
   * @return bool
   *   True if asymmetric translation is supported.
   */
  protected function supportsAsymmetricTranslations() {
    return $this->layoutParagraphsLayout->getParagraphsReferenceField()->getFieldDefinition()->isTranslatable();
  }

  /**
   * Adds a UI element to the Javascript settings array.
   *
   * Builder UI elements are attached to components (paragraphs)
   * in Javascript so that the UI is correctly rendered even when
   * the component (paragraph) template has been customized and the
   * contents of the content array are no longer output.
   *
   * @param array $build
   *   The build array to attach JS settings to.
   * @param \Drupal\Core\Render\Markup $element
   *   The UI element.
   * @param string $key
   *   The Javascript object key to use for storing the element.
   * @param string $method
   *   The javascript method to use to attach $element to its container.
   */
  public function addJsUiElement(array &$build, Markup $element, string $key, string $method = 'append') {
    $id = $build['#attributes']['data-lpb-ui-id'];
    $build['#attributes']['data-has-js-ui-element'] = TRUE;
    $build['#attached']['drupalSettings']['lpBuilder']['uiElements'][$id][$key] = [
      'element' => $element,
      'method' => $method,
    ];
  }

  /**
   * Processes a render array to markup.
   *
   * @param array $render_array
   *   The render array to process.
   *
   * @return \Drupal\Core\Render\Markup
   *   The markup object.
   */
  public function doRender(array $render_array) {
    return $this->renderer->render($render_array);
  }

  /**
   * Checks if adding a component would exceed the field's cardinality limit.
   *
   * @return bool
   *   True if a component can be added without exceeding cardinality.
   */
  protected function checkCardinality() {
    $cardinality = $this->getCardinality();
    if ($cardinality > 0) {
      $count = $this->layoutParagraphsLayout->getParagraphsReferenceField()->count();
      return $cardinality > $count;
    }
    return TRUE;
  }

  /**
   * Gets the cardinality field setting for a Layout Paragraphs reference field.
   *
   * @return int
   *   The cardinality setting.
   */
  protected function getCardinality() {
    $field_name = $this->layoutParagraphsLayout->getFieldName();
    $field_config = $this->layoutParagraphsLayout->getEntity()->{$field_name}->getFieldDefinition();
    $field_definition = $field_config->getFieldStorageDefinition();
    return $field_definition->getCardinality();
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc