layout_paragraphs-1.0.x-dev/src/Controller/ChooseComponentController.php
src/Controller/ChooseComponentController.php
<?php
namespace Drupal\layout_paragraphs\Controller;
use Drupal\Core\Url;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Ajax\OpenDialogCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\layout_paragraphs\Utility\Dialog;
use Symfony\Component\HttpFoundation\Request;
use Drupal\layout_paragraphs\LayoutParagraphsLayout;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\layout_paragraphs\Event\LayoutParagraphsComponentDefaultsEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Drupal\layout_paragraphs\LayoutParagraphsLayoutRefreshTrait;
use Drupal\layout_paragraphs\Event\LayoutParagraphsAllowedTypesEvent;
/**
* ChooseComponentController controller class.
*
* Returns a list of links for available component types that can
* be added to a layout region.
*/
class ChooseComponentController extends ControllerBase {
use AjaxHelperTrait;
use LayoutParagraphsLayoutRefreshTrait;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The event dispatcher service.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Construct a Layout Paragraphs Editor controller.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher_service
* The event dispatcher service.
*/
public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info, EventDispatcherInterface $event_dispatcher_service) {
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->eventDispatcher = $event_dispatcher_service;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.bundle.info'),
$container->get('event_dispatcher')
);
}
/**
* Builds the component menu.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request.
* @param \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout_paragraphs_layout
* The layout paragraphs layout object.
*
* @return array
* The build array.
*/
public function list(Request $request, LayoutParagraphsLayout $layout_paragraphs_layout) {
$context = $this->getContextFromRequest($request);
// If inserting a new item adjacent to a sibling component, the region
// passed in the URL will be incorrect if the existing sibling component
// was dragged into another region. In that case, always use the existing
// sibling's region.
if ($context['sibling_uuid']) {
$sibling = $layout_paragraphs_layout->getComponentByUuid($context['sibling_uuid']);
if (!$sibling) {
$error_details = [
'sibling_uuid' => $context['sibling_uuid'],
'request_url' => $request->getUri(),
'request_params' => $request->query->all(),
'full_context' => $context,
'layout_id' => $layout_paragraphs_layout->id(),
];
throw new \InvalidArgumentException('The sibling component with UUID ' . $context['sibling_uuid'] . ' does not exist. Request details: ' . json_encode($error_details));
}
$context['region'] = $sibling->getRegion();
}
$types = $this->getAllowedComponentTypes($layout_paragraphs_layout, $context);
// If there is only one type to render,
// return the component form instead of a list of links.
if (count($types) === 1) {
return $this->componentForm(key($types), $layout_paragraphs_layout, $context);
}
else {
return $this->componentMenu($types);
}
}
/**
* Returns a layout paragraphs component form using Ajax if appropriate.
*
* @param string $type_name
* The component (paragraph) type.
* @param \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout_paragraphs_layout
* The layout paragraphs layout object.
* @param array $context
* The context for the new component.
*
* @return \Drupal\Core\Ajax\AjaxResponse|array
* An ajax response or form render array.
*/
protected function componentForm(string $type_name, LayoutParagraphsLayout $layout_paragraphs_layout, array $context) {
// Dispatch a LayoutParagraphsComponentDefaultsEvent to allow other modules
// to alter the paragraph type and default values.
$event = new LayoutParagraphsComponentDefaultsEvent($type_name, []);
$this->eventDispatcher->dispatch($event, $event::EVENT_NAME);
$type = $this
->entityTypeManager()
->getStorage('paragraphs_type')
->load($event->getParagraphTypeId());
$form = $this->formBuilder()->getForm(
$this->getInsertComponentFormClass(),
$layout_paragraphs_layout,
$type,
$context['parent_uuid'],
$context['region'],
$context['sibling_uuid'],
$context['placement'],
$event->getDefaultValues()
);
if ($this->isAjax()) {
$response = new AjaxResponse();
$selector = Dialog::dialogSelector($layout_paragraphs_layout);
$response->addCommand(new OpenDialogCommand($selector, $form['#title'], $form, Dialog::dialogSettings()));
return $response;
}
return $form;
}
/**
* Returns a rendered menu of component types.
*
* @param array $types
* The component types.
*
* @return array
* The component menu render array.
*/
protected function componentMenu(array $types) {
foreach ($types as &$type) {
$type['url'] = $type['url_object']->toString();
$type['link_attributes'] = new Attribute([
'class' => ['use-ajax'],
]);
}
$section_components = array_filter($types, function ($type) {
return $type['is_section'] === TRUE;
});
$content_components = array_filter($types, function ($type) {
return $type['is_section'] === FALSE;
});
$empty_message = $this->config('layout_paragraphs.settings')->get('empty_message') ??
$this->t('No components to add.');
$component_menu = [
'#title' => $this->t('Choose a component'),
'#theme' => 'layout_paragraphs_builder_component_menu',
'#attributes' => [
'class' => ['lpb-component-list'],
],
'#empty_message' => $empty_message,
'#status_messages' => ['#type' => 'status_messages'],
'#types' => [
'layout' => $section_components,
'content' => $content_components,
],
'#attached' => [
'library' => [
'layout_paragraphs/component_list',
],
],
];
return $component_menu;
}
/**
* Returns an array that defines the context for the component being added.
*
* A context may have a parent_uuid and region, or a sibling_uuid and
* placement. If the former, the item is being inserted in the region of a
* layout component with the uuid of "parent_uuid". If the latter, the new
* component is being inserted alongside the component with the uuid of
* "sibling_uuid", based on the value in "placement" (before or after).
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return array
* The context into which the new component is being inserted.
*/
protected function getContextFromRequest(Request $request) {
return [
'parent_uuid' => $request->query->get('parent_uuid', NULL),
'region' => $request->query->get('region', NULL),
'sibling_uuid' => $request->query->get('sibling_uuid', NULL),
'placement' => $request->query->get('placement', NULL),
];
}
/**
* Returns an array of allowed component types.
*
* Dispatches a LayoutParagraphsComponentMenuEvent object so the component
* list can be manipulated based on the layout, the layout settings, the
* parent uuid, and region.
*
* @param \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout
* The layout object.
* @param array $context
* The context to be passed to the event.
*
* @return array[]
* Returns an array of allowed component types.
*/
public function getAllowedComponentTypes(LayoutParagraphsLayout $layout, array $context) {
// @todo Document and add tests for what is happening here.
$component_types = $this->getComponentTypes($layout, $context);
$event = new LayoutParagraphsAllowedTypesEvent($component_types, $layout, $context);
$this->eventDispatcher->dispatch($event, LayoutParagraphsAllowedTypesEvent::EVENT_NAME);
return $event->getTypes();
}
/**
* Returns an array of available component types.
*
* @param \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout
* The layout paragraphs layout.
* @param array $context
* The context for the component to be added.
*
* @return array
* An array of available component types.
*/
public function getComponentTypes(LayoutParagraphsLayout $layout, array $context) {
$items = $layout->getParagraphsReferenceField();
$settings = $items->getSettings()['handler_settings'];
$sorted_bundles = $this->getSortedAllowedTypes($settings);
$storage = $this->entityTypeManager()->getStorage('paragraphs_type');
$types = [];
foreach (array_keys($sorted_bundles) as $bundle) {
if (TRUE === $this->entityTypeManager->getAccessControlHandler('paragraph')->createAccess($bundle)) {
/** @var \Drupal\paragraphs\Entity\ParagraphsType $paragraphs_type */
$paragraphs_type = $storage->load($bundle);
$plugins = $paragraphs_type->getEnabledBehaviorPlugins();
$section_component = isset($plugins['layout_paragraphs']);
$path = '';
// Get the icon and pass to Javascript.
if (method_exists($paragraphs_type, 'getIconUrl')) {
$path = $paragraphs_type->getIconUrl();
}
$route_params = [
'layout_paragraphs_layout' => $layout->id(),
'paragraph_type_id' => $paragraphs_type->id(),
];
$types[$bundle] = [
'id' => $paragraphs_type->id(),
'label' => $paragraphs_type->label(),
'image' => $path,
'description' => $paragraphs_type->getDescription(),
'is_section' => $section_component,
'url_object' => Url::fromRoute('layout_paragraphs.builder.insert', $route_params, ['query' => $context]),
];
}
}
return $types;
}
/**
* Returns an array of sorted allowed component / paragraph types.
*
* @param array $settings
* The handler settings.
*
* @return array
* An array of sorted, allowed paragraph bundles.
*/
protected function getSortedAllowedTypes(array $settings) {
$bundles = $this->entityTypeBundleInfo->getBundleInfo('paragraph');
$return_bundles = [];
if (!empty($settings['target_bundles'])) {
if (isset($settings['negate']) && $settings['negate'] == '1') {
$bundles = array_diff_key($bundles, $settings['target_bundles']);
}
else {
$bundles = array_intersect_key($bundles, $settings['target_bundles']);
}
}
// Support for the paragraphs reference type.
if (!empty($settings['target_bundles_drag_drop'])) {
$drag_drop_settings = $settings['target_bundles_drag_drop'];
$max_weight = count($bundles);
foreach ($drag_drop_settings as $bundle_info) {
if (isset($bundle_info['weight']) && $bundle_info['weight'] && $bundle_info['weight'] > $max_weight) {
$max_weight = $bundle_info['weight'];
}
}
// Default weight for new items.
$weight = $max_weight + 1;
foreach ($bundles as $machine_name => $bundle) {
$return_bundles[$machine_name] = [
'label' => $bundle['label'],
'weight' => $drag_drop_settings[$machine_name]['weight'] ?? $weight,
];
$weight++;
}
}
else {
$weight = 0;
foreach ($bundles as $machine_name => $bundle) {
$return_bundles[$machine_name] = [
'label' => $bundle['label'],
'weight' => $weight,
];
$weight++;
}
}
uasort($return_bundles, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
return $return_bundles;
}
/**
* Returns the insert component form class.
*/
protected function getInsertComponentFormClass() {
return '\Drupal\layout_paragraphs\Form\InsertComponentForm';
}
}
