layout_paragraphs-1.0.x-dev/src/Form/ComponentFormBase.php
src/Form/ComponentFormBase.php
<?php
namespace Drupal\layout_paragraphs\Form;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Layout\LayoutPluginManagerInterface;
use Drupal\field_group\FormatterHelper;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\layout_paragraphs\Utility\Dialog;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\paragraphs\ParagraphInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\layout_paragraphs\Contracts\ComponentFormInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\layout_paragraphs\LayoutParagraphsLayoutRefreshTrait;
use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
/**
* Class LayoutParagraphsComponentFormBase.
*
* Base form for Layout Paragraphs component forms.
*/
abstract class ComponentFormBase extends FormBase implements ComponentFormInterface {
use AjaxFormHelperTrait;
use LayoutParagraphsLayoutRefreshTrait;
/**
* The tempstore service.
*
* @var \Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository
*/
protected $tempstore;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The layout plugin manager service.
*
* @var \Drupal\Core\Layout\LayoutPluginManagerInterface
*/
protected $layoutPluginManager;
/**
* The paragraph type.
*
* @var \Drupal\paragraphs\Entity\ParagraphsType
*/
protected $paragraphType;
/**
* The paragraph.
*
* @var \Drupal\paragraphs\ParagraphInterface
*/
protected $paragraph;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity repository service.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The form mode to use for rendering the form.
*
* @var string
*/
protected $formMode = 'default';
/**
* {@inheritdoc}
*/
public function __construct(
LayoutParagraphsLayoutTempstoreRepository $tempstore,
EntityTypeManagerInterface $entity_type_manager,
LayoutPluginManagerInterface $layout_plugin_manager,
ModuleHandlerInterface $module_handler,
EventDispatcherInterface $event_dispatcher,
EntityRepositoryInterface $entity_repository,
) {
$this->tempstore = $tempstore;
$this->entityTypeManager = $entity_type_manager;
$this->layoutPluginManager = $layout_plugin_manager;
$this->moduleHandler = $module_handler;
$this->eventDispatcher = $event_dispatcher;
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_paragraphs.tempstore_repository'),
$container->get('entity_type.manager'),
$container->get('plugin.manager.core.layout'),
$container->get('module_handler'),
$container->get('event_dispatcher'),
$container->get('entity.repository')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_paragraphs_component_form';
}
/**
* {@inheritdoc}
*/
public function getParagraph() {
return $this->paragraph;
}
/**
* {@inheritdoc}
*/
public function setParagraph(ParagraphInterface $paragraph) {
$this->paragraph = $paragraph;
}
/**
* {@inheritdoc}
*/
public function getLayoutParagraphsLayout() {
return $this->layoutParagraphsLayout;
}
/**
* Builds a component (paragraph) edit form.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
* @param string $form_display_mode
* The form display mode.
*/
protected function buildComponentForm(
array $form,
FormStateInterface $form_state,
string $form_display_mode = 'default',
) {
$this->initFormLangcodes($form_state);
$display = EntityFormDisplay::collectRenderDisplay($this->paragraph, $form_display_mode);
$display->buildForm($this->paragraph, $form, $form_state);
$this->paragraphType = $this->paragraph->getParagraphType();
$lp_config = $this->config('layout_paragraphs.settings');
$form += [
'#title' => $this->formTitle(),
'#paragraph' => $this->paragraph,
'#display' => $display,
'#tree' => TRUE,
'#after_build' => [
[$this, 'afterBuild'],
],
'#attached' => [
'library' => [
'layout_paragraphs/component_form',
],
],
'actions' => [
'#weight' => 100,
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#weight' => 100,
'#value' => $this->t('Save'),
'#ajax' => [
'callback' => '::ajaxSubmit',
'progress' => 'none',
],
'#attributes' => [
'class' => ['lpb-btn--save', 'button--primary'],
'data-disable-refocus' => 'true',
],
],
'cancel' => [
'#type' => 'button',
'#weight' => 200,
'#value' => $this->t('Cancel'),
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => '::cancel',
'progress' => 'none',
],
'#attributes' => [
'class' => [
'dialog-cancel',
'lpb-btn--cancel',
],
],
],
],
];
if ($this->paragraphType->hasEnabledBehaviorPlugin('layout_paragraphs')) {
$form['layout_paragraphs'] = [
'#process' => [
[$this, 'layoutParagraphsBehaviorForm'],
],
];
}
if (count($this->getEnabledBehaviorPlugins())) {
$form['behavior_plugins'] = [
'#weight' => $lp_config->get('paragraph_behaviors_position') ?? -99,
'#type' => 'details',
'#title' => $lp_config->get('paragraph_behaviors_label') ?? $this->t('Behaviors'),
'#process' => [
[$this, 'behaviorPluginsForm'],
],
];
}
// Support for Field Group module based on Paragraphs module.
// @todo Remove as part of https://www.drupal.org/node/2640056
if ($this->moduleHandler->moduleExists('field_group')) {
$context = [
'entity_type' => $this->paragraph->getEntityTypeId(),
'bundle' => $this->paragraph->bundle(),
'entity' => $this->paragraph,
'context' => 'form',
'display_context' => 'form',
'mode' => $display->getMode(),
];
// phpcs:ignore
field_group_attach_groups($form, $context);
if (method_exists(FormatterHelper::class, 'formProcess')) {
$form['#process'][] = [FormatterHelper::class, 'formProcess'];
}
elseif (function_exists('field_group_form_pre_render')) {
$form['#pre_render'][] = 'field_group_form_pre_render';
}
elseif (function_exists('field_group_form_process')) {
$form['#process'][] = 'field_group_form_process';
}
}
return $form;
}
/**
* Validate the component form.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// First validate paragraph behavior forms.
foreach ($this->getEnabledBehaviorPlugins() as $behavior_id => $behavior_plugin) {
if (!empty($form['behavior_plugins'][$behavior_id])) {
$subform_state = SubformState::createForSubform($form['behavior_plugins'][$behavior_id], $form_state->getCompleteForm(), $form_state);
$behavior_plugin->validateBehaviorForm($this->paragraph, $form['behavior_plugins'][$behavior_id], $subform_state);
}
}
// Validate the paragraph with submitted form values.
$paragraph = $this->buildParagraphComponent($form, $form_state);
$violations = $paragraph->validate();
// Remove violations of inaccessible fields.
$violations->filterByFieldAccess($this->currentUser());
// The paragraph component was validated.
$paragraph->setValidationRequired(FALSE);
// Flag entity level violations.
foreach ($violations->getEntityViolations() as $violation) {
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
$form_state->setErrorByName('', $violation->getMessage());
}
$form['#display']->flagWidgetsErrorsFromViolations($violations, $form, $form_state);
}
/**
* Saves the paragraph component.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->setParagraph($this->buildParagraphComponent($form, $form_state));
}
/**
* {@inheritdoc}
*/
abstract public function successfulAjaxSubmit(array $form, FormStateInterface $form_state);
/**
* Builds the paragraph component using submitted form values.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return \Drupal\paragraphs\Entity\Paragraph
* The paragraph entity.
*/
public function buildParagraphComponent(array $form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form['#display'];
$paragraph = clone $this->paragraph;
$paragraph->getAllBehaviorSettings();
$paragraphs_type = $paragraph->getParagraphType();
if ($paragraphs_type->hasEnabledBehaviorPlugin('layout_paragraphs')) {
$layout_paragraphs_plugin = $paragraphs_type->getEnabledBehaviorPlugins()['layout_paragraphs'];
$subform_state = SubformState::createForSubform($form['layout_paragraphs'], $form, $form_state);
$layout_paragraphs_plugin->submitBehaviorForm($paragraph, $form['layout_paragraphs'], $subform_state);
}
foreach ($this->getEnabledBehaviorPlugins() as $behavior_id => $behavior_plugin) {
$subform_state = SubformState::createForSubform($form['behavior_plugins'][$behavior_id], $form, $form_state);
$behavior_plugin->submitBehaviorForm($paragraph, $form['behavior_plugins'][$behavior_id], $subform_state);
}
$paragraph->setNeedsSave(TRUE);
$display->extractFormValues($paragraph, $form, $form_state);
return $paragraph;
}
/**
* Create the form title.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The form title.
*/
protected function formTitle() {
return $this->t('Component form');
}
/**
* After build callback fixes issues with data-drupal-selector.
*
* See https://www.drupal.org/project/drupal/issues/2897377
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The form element.
*/
public function afterBuild(array $element, FormStateInterface $form_state) {
$parents = array_merge($element['#parents'], [
$this->getFormId(),
$element['#paragraph']->bundle(),
]);
$unprocessed_id = 'edit-' . implode('-', $parents);
$element['#attributes']['data-drupal-selector'] = Html::getId($unprocessed_id);
$element['#dialog_id'] = $unprocessed_id . '-dialog';
return $element;
}
/**
* Form #process callback.
*
* Renders the layout paragraphs behavior form for layout selection.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $form
* The complete form array.
*
* @return array
* The processed element.
*/
public function layoutParagraphsBehaviorForm(array $element, FormStateInterface $form_state, array &$form) {
$layout_paragraphs_plugin = $this->paragraphType->getEnabledBehaviorPlugins()['layout_paragraphs'];
$subform_state = SubformState::createForSubform($element, $form, $form_state);
if ($layout_paragraphs_plugin_form = $layout_paragraphs_plugin->buildBehaviorForm($this->paragraph, $element, $subform_state)) {
$element = $layout_paragraphs_plugin_form;
$element['layout']['#ajax']['callback'] = [$this, 'ajaxCallback'];
}
return $element;
}
/**
* Form #process callback.
*
* Attaches the behavior plugin forms.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $form
* The complete form array.
*
* @return array
* The processed element.
*/
public function behaviorPluginsForm(array $element, FormStateInterface $form_state, array &$form) {
$element['#type'] = 'container';
$element['#attributes']['class'][] = 'lpb-behavior-plugins';
foreach ($this->getEnabledBehaviorPlugins() as $behavior_id => $behavior_plugin) {
$element[$behavior_id] = [
'#parents' => array_merge($element['#parents'], [$behavior_id]),
'#type' => 'container',
'#attributes' => [
'class' => ['lpb-behavior-plugins__' . Html::cleanCssIdentifier($behavior_id)],
],
];
$subform_state = SubformState::createForSubform($element[$behavior_id], $form, $form_state);
if ($behavior_form = $behavior_plugin->buildBehaviorForm($this->paragraph, $element[$behavior_id], $subform_state)) {
$element[$behavior_id] = $behavior_form;
}
}
return $element;
}
/**
* Ajax form callback.
*
* Returns the layout paragraphs behavior form,
* which includes the orphaned items element when necessary.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The ajax response.
*/
public function ajaxCallback(array $form, FormStateInterface $form_state) {
$response = new AjaxResponse();
if (!empty($form['layout_paragraphs']['config'])) {
$selector = $form['layout_paragraphs']['config']['#attributes']['data-drupal-selector'];
$response->addCommand(new ReplaceCommand(
'[data-drupal-selector="' . $selector . '"]',
$form['layout_paragraphs']['config']
));
}
return $response;
}
/**
* Form #ajax callback.
*
* Cancels the edit operation and closes the dialog.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The Ajax response.
*/
public function cancel(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$this->ajaxCloseForm($response);
return $response;
}
/**
* Access check.
*
* @todo Actually check something.
*
* @return bool
* True if access.
*/
public function access() {
return AccessResult::allowed();
}
/**
* Closes the form with ajax.
*
* @param \Drupal\Core\Ajax\AjaxResponse $response
* The ajax response.
*/
protected function ajaxCloseForm(AjaxResponse &$response) {
$selector = Dialog::dialogSelector($this->layoutParagraphsLayout);
$response->addCommand(new CloseDialogCommand($selector));
}
/**
* Renders a single Layout Paragraphs Layout paragraph entity.
*
* @param string $uuid
* The uuid of the paragraph entity to render.
*
* @return array
* The paragraph render array.
*/
protected function renderParagraph(string $uuid) {
$this->layoutParagraphsLayout->setComponent($this->paragraph);
return [
'#type' => 'layout_paragraphs_builder',
'#layout_paragraphs_layout' => $this->layoutParagraphsLayout,
'#uuid' => $uuid,
'#cache' => [
'max-age' => 0,
],
];
}
/**
* Returns an array of region names for a given layout.
*
* @param string $layout_id
* The layout id.
*
* @return array
* An array of regions.
*/
protected function getLayoutRegionNames($layout_id) {
return array_map(function ($region) {
return $region['label'];
}, $this->getLayoutRegions($layout_id));
}
/**
* Returns an array of regions for a given layout.
*
* @param string $layout_id
* The layout id.
*
* @return array
* An array of regions.
*/
protected function getLayoutRegions($layout_id) {
if (!$layout_id) {
return [];
}
$instance = $this->layoutPluginManager->createInstance($layout_id);
$definition = $instance->getPluginDefinition();
return $definition->getRegions();
}
/**
* Initializes form language code values.
*
* See Drupal\Core\Entity\ContentEntityForm.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function initFormLangcodes(FormStateInterface $form_state) {
// Store the entity default language to allow checking whether the form is
// dealing with the original entity or a translation.
if (!$form_state
->has('entity_default_langcode')) {
$form_state
->set('entity_default_langcode', $this->paragraph
->getUntranslated()
->language()
->getId());
}
// This value might have been explicitly populated to work with a particular
// entity translation. If not we fall back to the most proper language based
// on contextual information.
if (!$form_state
->has('langcode')) {
// Imply a 'view' operation to ensure users edit entities in the same
// language they are displayed. This allows to keep contextual editing
// working also for multilingual entities.
$form_state
->set('langcode', $this->entityRepository
->getTranslationFromContext($this->paragraph)
->language()
->getId());
}
}
/**
* Returns an array of enabled behavior plugins excluding Layout Paragraphs.
*
* The Layout Paragraphs behavior plugin form is handled separately.
*
* @return array
* An array of enabled plugins.
*/
protected function getEnabledBehaviorPlugins() {
if ($this->currentUser()->hasPermission('edit behavior plugin settings')) {
return array_filter(
$this->paragraphType->getEnabledBehaviorPlugins(),
function ($key) {
return $key != 'layout_paragraphs';
},
ARRAY_FILTER_USE_KEY
);
}
return [];
}
/**
* {@inheritdoc}
*/
public function getFormMode() {
return $this->formMode;
}
/**
* {@inheritdoc}
*/
public function setFormMode($view_mode) {
$this->formMode = $view_mode;
}
}
