mercury_editor-2.0.x-dev/src/Entity/MercuryEditorEntityFormTrait.php
src/Entity/MercuryEditorEntityFormTrait.php
<?php
namespace Drupal\mercury_editor\Entity;
use Drupal\Core\Url;
use Drupal\Core\Render\Element;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\mercury_editor\Ajax\MercuryEditorSaveSuccessCommand;
/**
* Defines a trait for Mercury Editor entity forms.
*/
trait MercuryEditorEntityFormTrait {
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStackInterface
*/
protected $requestStack;
/**
* The mercury editor tray tempstore repository.
*
* @var \Drupal\mercury_editor\MercuryEditorTempstore
*/
protected $tempstore;
/**
* The layout paragraphs tempstore repository.
*
* @var \Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository
*/
protected $layoutParagraphsTempstore;
/**
* The iFrame Ajax Response Wrapper.
*
* @var \Drupal\mercury_editor\Ajax\IFrameAjaxResponseWrapper
*/
protected $iFrameAjaxResponseWrapper;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The Mercury Editor context service.
*
* @var \Drupal\mercury_editor\MercuryEditorContextService
*/
protected $mercuryEditorContextService;
/**
* The Mercury Editor preview service.
*
* @var \Drupal\mercury_editor\MercuryEditorPreviewService
*/
protected $mercuryEditorPreviewService;
/**
* The form builder service.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The fields to sync changes in UI.
*
* @var array
*/
protected $syncFields = ['name', 'title', 'label'];
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->injectDependencies($container);
return $instance;
}
/**
* Injects dependencies from the container.
*/
public function injectDependencies(ContainerInterface $container) {
$this->tempstore = $container->get('mercury_editor.tempstore_repository');
$this->layoutParagraphsTempstore = $container->get('layout_paragraphs.tempstore_repository');
$this->iFrameAjaxResponseWrapper = $container->get('mercury_editor.iframe_ajax_response_wrapper');
$this->entityTypeManager = $container->get('entity_type.manager');
$this->mercuryEditorContextService = $container->get('mercury_editor.context');
$this->mercuryEditorPreviewService = $container->get('mercury_editor.preview');
$this->requestStack = $container->get('request_stack');
$this->formBuilder = $container->get('form_builder');
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = $this->mercuryEditorPrepareForm($form, $form_state);
return $form;
}
/**
* Applies necessary changes to the form for use with Mercury Editor.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The altered form array.
*/
protected function mercuryEditorPrepareForm(array $form, FormStateInterface $form_state) {
// Set default values for a new entity.
// Specific Mercury Editor form classes should override this method to set
// default values for the entity.
if (!$form_state->get('init') && !$this->entity->id()) {
$this->setDefaultEntityValues();
}
$form = parent::form($form, $form_state);
$form['#theme'] = 'mercury_editor_entity_form';
$label = $this->getEntityBundleTypeLabel();
if ($this->entity->id()) {
$form['#title'] = $this->t('Edit @type', [
'@type' => $label,
]);
}
else {
$form['#title'] = $this->t('Create @type', [
'@type' => $label,
]);
}
$form['messages'] = [
'#type' => 'container',
'#attributes' => [
'class' => [
'me-form-messages',
],
],
'#weight' => -10,
];
$form['#attributes']['class'][] = 'me-entity-form';
$form['#attributes']['class'][] = 'me-autosave-form';
// Provides a redirect URL so Javascript behaviors can redirect the parent
// window to the correct destination after the editor is closed.
$form['redirect_url'] = [
'#type' => 'hidden',
'#attributes' => [
'class' => [
'me-edit-screen-redirect-url',
],
],
'#process' => [[$this, 'processRedirectUrl']],
];
// @todo Make this more general, perhaps moving into another module.
// Sync changes in UI.
foreach ($this->syncFields as $field_name) {
if (!empty($form[$field_name])) {
$form[$field_name]['widget'][0]['value']['#attributes']['data-sync-changes'] = implode('/', [
$this->entity->getEntityTypeId(),
$this->entity->uuid(),
$field_name,
]);
}
}
// Temporarily save a reference to the layout paragraphs layout builder in
// the entity so we can swap the field for the builder when rendering.
$lp_storage_keys = [];
foreach (Element::children($form, TRUE) as $field_name) {
if (isset($form[$field_name]['widget']['layout_paragraphs_storage_key'])) {
$lp_storage_keys[$field_name] = $form[$field_name]['widget']['layout_paragraphs_storage_key']['#default_value'];
// Hide the layout paragraphs field for rendering in the frontend.
$form[$field_name]['widget']['layout_paragraphs_builder']['#access'] = FALSE;
$form[$field_name]['widget']['#type'] = 'container';
// Set the Mercury Editor context on the layout.
/** @var \Drupal\layout_paragraphs\LayoutParagraphsLayout $layout */
$layout = $form[$field_name]['widget']['layout_paragraphs_builder']['#layout_paragraphs_layout'];
// Set referring items so we can save the layout to the correct entity.
$items = $layout->getParagraphsReferenceField();
foreach ($items as $key => $item) {
if (!empty($item->entity)) {
$items[$key]->entity->_referringItem = $items[$key];
}
}
$layout->setParagraphsReferenceField($items);
$settings = $layout->getSettings();
$settings['mercury_editor_context'] = TRUE;
$settings['is_translating'] = $form[$field_name]['widget']['layout_paragraphs_builder']['#is_translating'] ?? FALSE;
$layout->setSettings($settings);
$this->layoutParagraphsTempstore->set($layout);
}
}
if ($lp_storage_keys) {
$this->tempstore->setLayoutParagraphsFieldIds($this->entity, $lp_storage_keys);
}
if (!$form_state->get('init')) {
$form_state->set('init', TRUE);
}
$this->tempstore->set($this->entity);
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) : array {
$element = parent::actions($form, $form_state);
$element['autosave'] = [
'#type' => 'submit',
'#value' => $this->t('Save progress'),
'#submit' => [
'::submitForm',
'::saveProgress',
],
'#ajax' => [
'callback' => '::ajaxAutosaveCallback',
'prevent' => 'none',
],
'#attributes' => [
'class' => [
'me-autosave-btn',
],
],
];
if (isset($element['delete'])) {
$element['delete']['#attributes']['target'] = '_self';
$query = $element['delete']['#url']->getOption('query') ?? [];
$element['delete']['#url']->setOption('query', ['_mercury_editor_iframe' => TRUE] + $query);
}
$element['submit']['#access'] = TRUE;
$element['submit']['#ajax'] = [
'callback' => '::ajaxCallback',
];
$element['preview']['#access'] = FALSE;
$element['#attributes']['class'][] = 'me-form-actions';
return $element;
}
/**
* {@inheritDoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$form_state->setValue('changed', time());
return parent::validateForm($form, $form_state);
}
/**
* {@inheritDoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$form_state->setRebuild(TRUE);
}
/**
* Saves the entity to the tempstore.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function saveProgress(array $form, FormStateInterface $form_state) {
// Remove 'menu' from the form state to prevent menu validation errors.
// This is necessary because the menu_ui submit handler expects the
// entity to have an ID, and the entity may not have one yet.
// @todo Consider other fields that may need to be unset.
if (!$this->entity->id()) {
$form_state->unsetValue('menu');
}
$this->tempstore->set($this->entity);
$this->tempstore->saveState($this->entity);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$status = parent::save($form, $form_state);
// Reload the entity from storage to ensure all field values are correctly
// populated before re-rendering the form.
// @todo Consider moving this to a service as it is also used in
// MercuryEditorController::editor().
$entity_type_id = $this->entity->getEntityTypeId();
$entity_id = $this->entity->id();
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
$revision_id = $storage->getLatestRevisionId($entity_id);
$this->entity = !is_null($revision_id)
? $storage->loadRevision($revision_id)
: $storage->load($entity_id);
if ($this->entity instanceof ContentEntityInterface) {
if ($form_state->get('langcode') !== $this->entity->language()->getId()) {
$this->entity = $this->entity->getTranslation($form_state->get('langcode'));
}
}
$this->tempstore->set($this->entity);
$this->messenger()->deleteAll();
// Clear the revision log message input to prevent it from being reused.
$entity_type = $this->entity->getEntityType();
if ($entity_type instanceof ContentEntityTypeInterface) {
$revision_field = $entity_type->getRevisionMetadataKey('revision_log_message');
if ($revision_field) {
$input = $form_state->getUserInput();
$input[$revision_field] = [];
$form_state->setUserInput($input);
}
}
// Re-initialize the form to correctly prepare the saved entity.
$this->init($form_state);
// Clear the tempstore states for the entity.
$this->tempstore->clearStates($this->entity);
return $status;
}
/**
* Get the entity type label.
*
* @return string
* The label.
*/
protected function getEntityBundleTypeLabel() {
$type = $this->entity->getEntityType();
$bundle_entity_type = $type->get('bundle_entity_type');
if ($bundle_entity_type) {
$bundle = $this->entity->bundle();
$bundle_object = $this->entityTypeManager
->getStorage($bundle_entity_type)
->load($bundle);
$label = $bundle_object ? $bundle_object->label() : '';
}
else {
$label = $type->getLabel();
}
return $label;
}
/**
* Adds messages to an ajax response.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Ajax\AjaxResponse $response
* The ajax response.
*/
protected function ajaxAddMessages(array $form, FormStateInterface $form_state, AjaxResponse $response) {
$response->addCommand(new RemoveCommand('.me-form-messages .messages'));
foreach ($this->messenger()->all() as $type => $messages) {
foreach ($messages as $message) {
$response->addCommand(new MessageCommand($message, '.me-entity-form .me-form-messages', ['type' => $type], FALSE));
}
}
$this->messenger()->deleteAll();
}
/**
* AJAX callback for the autosave button.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function ajaxAutosaveCallback(array &$form, FormStateInterface $form_state) {
$response = $this->mercuryEditorPreviewService->ajaxUpdatePreview($this->entity);
$this->ajaxAddMessages($form, $form_state, $response);
return $response;
}
/**
* Ajax callback for rendering the form.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function ajaxCallback(array &$form, FormStateInterface $form_state) {
$form['#updated'] = TRUE;
$response = $this->mercuryEditorPreviewService->ajaxUpdatePreview($this->entity);
$response->addCommand(new ReplaceCommand('.me-entity-form', $form));
$this->ajaxAddMessages($form, $form_state, $response);
if (!$form_state->hasAnyErrors()) {
$response->addCommand(new MercuryEditorSaveSuccessCommand());
}
return $response;
}
/**
* Process callback for setting the redirect URL.
*
* @param array $element
* The form element.
* @param FormstateInterface $form_state
* The form state.
*
* @return array
* The processed element.
*/
public function processRedirectUrl(array $element, FormstateInterface $form_state) {
$entity = $this->tempstore->get($this->entity->uuid());
$request = $this->requestStack->getCurrentRequest();
if ($request->get('destination')) {
$element['#value'] = $request->get('destination');
}
elseif ($entity->id()) {
if ($entity instanceof RevisionableInterface && ($entity->isDefaultRevision() || $entity->isLatestRevision())) {
$element['#value'] = $entity->toUrl('canonical', ['absolute' => TRUE])->toString();
}
else {
$element['#value'] = $entity->toUrl('latest-version', ['absolute' => TRUE])->toString();
}
}
else {
$element['#value'] = Url::fromRoute('node.add_page', [], ['absolute' => TRUE])->toString();
}
return $element;
}
/**
* Set default values for the entity.
*/
public function setDefaultEntityValues() {
if (method_exists(get_parent_class($this), 'setDefaultEntityValues')) {
return parent::setDefaultEntityValues();
}
}
}
