EditFieldForm.php 8.74 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php

/**
 * @file
 * Contains \Drupal\edit\Form\EditFieldForm.
 */

namespace Drupal\edit\Form;

10 11
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
12
use Drupal\Core\Entity\EntityInterface;
13 14 15
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormInterface;
16
use Drupal\user\TempStoreFactory;
17
use Drupal\Core\Entity\EntityChangedInterface;
18 19 20 21

/**
 * Builds and process a form for editing a single entity field.
 */
22
class EditFieldForm implements FormInterface, ContainerInjectionInterface {
23

24 25 26 27 28 29 30
  /**
   * Stores the tempstore factory.
   *
   * @var \Drupal\user\TempStoreFactory
   */
  protected $tempStoreFactory;

31
  /**
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The node type storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageControllerInterface
   */
  protected $nodeTypeStorage;

  /**
   * Constructs a new EditFieldForm.
   *
   * @param \Drupal\user\TempStoreFactory $temp_store_factory
   *   The tempstore factory.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Entity\EntityStorageControllerInterface $node_type_storage
   *   The node type storage.
   */
  public function __construct(TempStoreFactory $temp_store_factory, ModuleHandlerInterface $module_handler, EntityStorageControllerInterface $node_type_storage) {
    $this->moduleHandler = $module_handler;
    $this->nodeTypeStorage = $node_type_storage;
    $this->tempStoreFactory = $temp_store_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('user.tempstore'),
      $container->get('module_handler'),
      $container->get('plugin.manager.entity')->getStorageController('node_type')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormID() {
    return 'edit_field_form';
  }

  /**
   * {@inheritdoc}
   *
82 83
   * Builds a form for a single entity field.
   */
84
  public function buildForm(array $form, array &$form_state, EntityInterface $entity = NULL, $field_name = NULL) {
85 86 87 88 89
    if (!isset($form_state['entity'])) {
      $this->init($form_state, $entity, $field_name);
    }

    // Add the field form.
90
    field_attach_form($form_state['entity'], $form, $form_state, $form_state['langcode'], array('field_name' =>  $form_state['field_name']));
91

92 93 94 95 96 97 98 99
    // Add a dummy changed timestamp field to attach form errors to.
    if ($entity instanceof EntityChangedInterface) {
      $form['changed_field'] = array(
        '#type' => 'hidden',
        '#value' => $entity->getChangedTime(),
      );
    }

100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
    // Add a submit button. Give it a class for easy JavaScript targeting.
    $form['actions'] = array('#type' => 'actions');
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Save'),
      '#attributes' => array('class' => array('edit-form-submit')),
    );

    // Simplify it for optimal in-place use.
    $this->simplify($form, $form_state);

    return $form;
  }

  /**
   * Initialize the form state and the entity before the first form build.
   */
  protected function init(array &$form_state, EntityInterface $entity, $field_name) {
    // @todo Rather than special-casing $node->revision, invoke prepareEdit()
    //   once http://drupal.org/node/1863258 lands.
    if ($entity->entityType() == 'node') {
121 122 123
      $node_type_settings = $this->nodeTypeStorage->load($entity->bundle())->getModuleSettings('node');
      $options = (isset($node_type_settings['options'])) ? $node_type_settings['options'] : array();
      $entity->setNewRevision(in_array('revision', $options));
124 125 126 127 128
      $entity->log = NULL;
    }

    $form_state['entity'] = $entity;
    $form_state['field_name'] = $field_name;
129 130 131 132 133 134 135 136 137 138 139

    // @todo Allow the usage of different form modes by exposing a hook and the
    // UI for them.
    $form_display = entity_get_render_form_display($entity, 'default');

    // Let modules alter the form display.
    $form_display_context = array(
      'entity_type' => $entity->entityType(),
      'bundle' => $entity->bundle(),
      'form_mode' => 'default',
    );
140
    $this->moduleHandler->alter('entity_form_display', $form_display, $form_display_context);
141 142

    $form_state['form_display'] = $form_display;
143 144 145
  }

  /**
146
   * {@inheritdoc}
147
   */
148
  public function validateForm(array &$form, array &$form_state) {
149
    $entity = $this->buildEntity($form, $form_state);
150
    field_attach_form_validate($entity, $form, $form_state, array('field_name' =>  $form_state['field_name']));
151 152 153 154 155 156 157 158 159 160

    // Do validation on the changed field as well and assign the error to the
    // dummy form element we added for this. We don't know the name of this
    // field on the entity, so we need to find it and validate it ourselves.
    if ($changed_field_name = $this->getChangedFieldName($entity)) {
      $changed_field_errors = $entity->$changed_field_name->validate();
      if (count($changed_field_errors)) {
        form_set_error('changed_field', $changed_field_errors[0]->getMessage());
      }
    }
161 162 163
  }

  /**
164 165
   * {@inheritdoc}
   *
166 167
   * Saves the entity with updated values for the edited field.
   */
168
  public function submitForm(array &$form, array &$form_state) {
169
    $form_state['entity'] = $this->buildEntity($form, $form_state);
170 171 172

    // Store entity in tempstore with its UUID as tempstore key.
    $this->tempStoreFactory->get('edit')->set($form_state['entity']->uuid(), $form_state['entity']);
173 174 175 176 177 178 179 180 181 182 183
  }

  /**
   * Returns a cloned entity containing updated field values.
   *
   * Calling code may then validate the returned entity, and if valid, transfer
   * it back to the form state and save it.
   */
  protected function buildEntity(array $form, array &$form_state) {
    $entity = clone $form_state['entity'];

184
    field_attach_extract_form_values($entity, $form, $form_state, array('field_name' =>  $form_state['field_name']));
185 186 187 188 189

    // @todo Refine automated log messages and abstract them to all entity
    //   types: http://drupal.org/node/1678002.
    if ($entity->entityType() == 'node' && $entity->isNewRevision() && !isset($entity->log)) {
      $instance = field_info_instance($entity->entityType(), $form_state['field_name'], $entity->bundle());
190
      $entity->log = t('Updated the %field-name field through in-place editing.', array('%field-name' => $instance->getFieldLabel()));
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
    }

    return $entity;
  }

  /**
   * Simplifies the field edit form for in-place editing.
   *
   * This function:
   * - Hides the field label inside the form, because JavaScript displays it
   *   outside the form.
   * - Adjusts textarea elements to fit their content.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   */
  protected function simplify(array &$form, array &$form_state) {
    $field_name = $form_state['field_name'];
209
    $widget_element =& $form[$field_name]['widget'];
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234

    // Hide the field label from displaying within the form, because JavaScript
    // displays the equivalent label that was provided within an HTML data
    // attribute of the field's display element outside of the form. Do this for
    // widgets without child elements (like Option widgets) as well as for ones
    // with per-delta elements. Skip single checkboxes, because their title is
    // key to their UI. Also skip widgets with multiple subelements, because in
    // that case, per-element labeling is informative.
    $num_children = count(element_children($widget_element));
    if ($num_children == 0 && $widget_element['#type'] != 'checkbox') {
      $widget_element['#title_display'] = 'invisible';
    }
    if ($num_children == 1 && isset($widget_element[0]['value'])) {
      // @todo While most widgets name their primary element 'value', not all
      //   do, so generalize this.
      $widget_element[0]['value']['#title_display'] = 'invisible';
    }

    // Adjust textarea elements to fit their content.
    if (isset($widget_element[0]['value']['#type']) && $widget_element[0]['value']['#type'] == 'textarea') {
      $lines = count(explode("\n", $widget_element[0]['value']['#default_value']));
      $widget_element[0]['value']['#rows'] = $lines + 1;
    }
  }

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
  /**
   * Finds the field name for the field carrying the changed timestamp, if any.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity.
   *
   * @return string|null
   *   The name of the field found or NULL if not found.
   */
  protected function getChangedFieldName(EntityInterface $entity) {
    foreach ($entity as $field_name => $field) {
      $definition = $field->getDefinition();
      if (isset($definition['property_constraints']['value']['EntityChanged'])) {
        return $field_name;
      }
    }
  }

253
}