panopoly_magic-8.x-2.x-dev/src/Form/LayoutBuilderBlockFormTrait.php
src/Form/LayoutBuilderBlockFormTrait.php
<?php
namespace Drupal\panopoly_magic\Form;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\block_content\Plugin\Block\BlockContentBlock;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\PreviewFallbackInterface;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\panopoly_magic\Alterations\ReusableBlocks;
use Drupal\panopoly_magic\Event\PanopolyMagicLivePreviewEvent;
use Drupal\panopoly_magic\PanopolyMagicEvents;
use Drupal\views\Plugin\Block\ViewsBlock;
/**
* Trait with methods for the add and update forms in layout builder.
*/
trait LayoutBuilderBlockFormTrait {
/**
* Gets the configured live preview mode.
*
* @return string
* The live preview mode: automatic, manual or disabled.
*/
protected function getLivePreviewMode() {
return \Drupal::config('panopoly_magic.settings')->get('live_preview');
}
/**
* Gets the event dispatcher.
*
* @return \Symfony\Component\EventDispatcher\EventDispatcher
* The event dispatcher.
*/
protected function getEventDispatcher() {
return \Drupal::service('event_dispatcher');
}
/**
* Gets the plugin definition for the block we are configuring.
*
* This will get any layout_builder-specific customizations included.
*
* @return array
* The plugin definition.
*/
protected function getBlockPluginDefinition() {
// First, get the definition as filtered for layout builder, so we can get
// any customizations.
$plugin_id = $this->block->getPluginId();
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', [], ['list' => 'inline_blocks']);
if (isset($definitions[$plugin_id])) {
return $definitions[$plugin_id];
}
// But, if it doesn't exist (because it was hidden, possibly via Panopoly
// Admin), then use the upstream definition.
return $this->block->getPluginDefinition();
}
/**
* Alters the form to add the preview elements.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
protected function alterFormForPreview(array &$form, FormStateInterface $form_state) {
if ($this->getLivePreviewMode() === 'disabled') {
return;
}
// Put the action buttons in the bottom of the dialog.
$form['actions']['#type'] = 'container';
$form['actions']['#attributes']['class'][] = 'form-actions';
// Add the preview button.
$form['actions']['preview'] = [
'#type' => 'button',
'#value' => $this->t('Preview'),
'#attributes' => [
'class' => [
'panopoly-magic-live-preview',
],
],
'#ajax' => [
'callback' => '::ajaxSubmit',
'disable-refocus' => TRUE,
],
];
// Add a special class to all the buttons so ONLY they'll get moved to the
// bottom of the dialog.
foreach (Element::children($form['actions']) as $name) {
$form['actions'][$name]['#attributes']['class'][] = 'js-panopoly-magic-live-preview-button';
}
}
/**
* Suppresses form validate for preview.
*
* @param array $form
* The form.
* @param Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
protected function suppressValidationForPreview(array &$form, FormStateInterface $form_state) {
// Suppress form validation errors when using automatic preview, allowing
// partial previews and removing error messages when a block has multiple
// required fields.
$submit_button_name = end($form_state->getTriggeringElement()['#parents']);
if ($submit_button_name == 'preview' && $this->getLivePreviewMode() === "automatic") {
// Suppress all future validation errors from parent::validateForm().
$form_state->setLimitValidationErrors([]);
// Capture any errors so we can show them to the user.
$form_state->setTemporaryValue('panopoly_magic_preview_errors', $form_state->getErrors());
// Clear any existing validation errors from the Field API.
$form_state->clearErrors();
// Prevent caching the form from preview.
$form_state->disableCache();
}
}
/**
* Creates AJAX responses to rebuild the preview.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to rebuild the preview.
*/
protected function rebuildPreview(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
// If there's form validation errors, show them instead of the preview.
if ($form_state->hasTemporaryValue('panopoly_magic_preview_errors')) {
$errors = $form_state->getTemporaryValue('panopoly_magic_preview_errors');
if (!empty($errors)) {
$response->addCommand(new ReplaceCommand('#panopoly-magic-preview', $this->buildPreviewError($errors)));
return $response;
}
}
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$subform_obj = $this->getPluginForm($this->block);
// Call the plugin validation handler (includes entity validation when
// placing content blocks).
$subform_state->setValidationComplete(FALSE);
$subform_obj->validateConfigurationForm($form['settings'], $subform_state);
$subform_state->setValidationComplete(TRUE);
// If there's plugin validation errors, show them instead of the preview.
if ($subform_state->hasAnyErrors()) {
$response->addCommand(new ReplaceCommand('#panopoly-magic-preview', $this->buildPreviewError($subform_state->getErrors())));
return $response;
}
// Call the plugin submit handler.
if ($subform_state->hasTemporaryValue('block_form_parents')) {
// Adjust a temporary value set by the block form so it can work correctly
// as a sub-form.
$block_form_parents = $subform_state->getTemporaryValue('block_form_parents');
if ($block_form_parents[0] === 'settings') {
array_shift($block_form_parents);
}
$subform_state->setTemporaryValue('block_form_parents', $block_form_parents);
}
$subform_obj->submitConfigurationForm($form['settings'], $subform_state);
// If this block is context-aware, set the context mapping.
if ($this->block instanceof ContextAwarePluginInterface) {
$this->block->setContextMapping($subform_state->getValue('context_mapping', []));
}
// If this is a content block, then we need to also validate and submit the
// content entity's form (but not save it).
if ($this->block instanceof BlockContentBlock && !empty($form['block_form'])) {
ReusableBlocks::blockContentValidate($form, $form_state);
ReusableBlocks::blockContentSubmit($form, $form_state, FALSE);
}
$response->addCommand(new ReplaceCommand('#panopoly-magic-preview', $this->buildPreview($form_state->getValues())));
return $response;
}
/**
* Builds the preview render array for the current block.
*/
public function buildPreview(array $form_values = []) {
if ($this->block instanceof BlockContentBlock) {
// For content blocks, we need to reuse the same instance of the block
// because it'll have updated content entity on it.
$block = $this->block;
}
else {
// Create a fresh instance of all other blocks, because there may be some
// lingering state on the old block instance due to creating and
// submitting the form.
$block = $this->blockManager->createInstance($this->block->getPluginId(), $this->block->getConfiguration());
}
if ($block instanceof RefinableDependentAccessInterface) {
$block->setAccessDependency(new LayoutPreviewAccessAllowed());
}
if ($block instanceof ViewsBlock) {
$block->getViewExecutable()->setShowAdminLinks(FALSE);
}
if ($block instanceof ContextAwarePluginInterface) {
$storage_contexts = $this->sectionStorage->getContexts();
foreach ($storage_contexts as $context_name => $context) {
$block->setContext($context_name, $context);
}
}
try {
$content = $block->build();
unset($content['#contextual_links']);
}
catch (\Exception $e) {
$content = [];
}
$cache = new CacheableMetadata();
$cache->addCacheableDependency(AccessResult::allowed()->setCacheMaxAge(0));
$cache->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
if (Element::isEmpty($content)) {
$fallback_string = '';
if ($block instanceof PreviewFallbackInterface) {
try {
$fallback_string = $block->getPreviewFallbackString();
}
catch (ContextException $e) {
// Leave empty. We'll assign a default below.
}
}
if (!$fallback_string) {
$fallback_string = $this->t("Preview unavailable.");
}
$content = [
'#markup' => $fallback_string,
];
}
else {
$content = [
'#theme' => 'block',
'#attributes' => [],
'#configuration' => $block->getConfiguration(),
'#plugin_id' => $block->getPluginId(),
'#base_plugin_id' => $block->getBaseId(),
'#derivative_plugin_id' => $block->getDerivativeId(),
'content' => $content,
];
}
// Dispatch event so other modules can modify the preview.
$event = new PanopolyMagicLivePreviewEvent($block->getPluginId(), $block->getConfiguration(), $content, $this->getCurrentComponent(), $form_values);
/** @var \Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher */
$event_dispatcher = $this->getEventDispatcher();
$event_dispatcher->dispatch($event, PanopolyMagicEvents::LIVE_PREVIEW_EVENT);
$preview = [
'#theme' => 'panopoly_magic_preview',
'#title' => $this->t("Preview"),
'#attributes' => [
'id' => 'panopoly-magic-preview',
],
'#weight' => -100,
'preview' => $event->getPreview(),
];
if ($this->getLivePreviewMode() === 'automatic') {
$preview['#attached']['library'][] = 'panopoly_magic/preview.live.automatic';
}
else {
$preview['#attached']['library'][] = 'panopoly_magic/preview.live.manual';
}
$cache->applyTo($preview);
return $preview;
}
/**
* Builds preview render array with error messages.
*
* @param string[] $errors
* Array of error messages to show.
*/
protected function buildPreviewError(array $errors) {
$preview = [
'#theme' => 'panopoly_magic_preview',
'#title' => $this->t("Preview"),
'#attributes' => [
'id' => 'panopoly-magic-preview',
],
'#cache' => [
'max-age' => 0,
],
'preview' => [
'#theme' => 'status_messages',
'#message_list' => [
'error' => $errors,
],
],
];
return $preview;
}
}
