WidgetBase.php 18.7 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\Field\WidgetBase.
6 7
 */

8
namespace Drupal\Core\Field;
9

10
use Drupal\Component\Utility\NestedArray;
11
use Drupal\Component\Utility\SortArray;
12
use Drupal\Component\Utility\String;
13
use Symfony\Component\Validator\ConstraintViolationInterface;
14
use Symfony\Component\Validator\ConstraintViolationListInterface;
15 16 17

/**
 * Base class for 'Field widget' plugin implementations.
18 19
 *
 * @ingroup field_widget
20 21 22 23 24 25
 */
abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface {

  /**
   * The field definition.
   *
26
   * @var \Drupal\Core\Field\FieldDefinitionInterface
27
   */
28
  protected $fieldDefinition;
29 30 31 32 33 34 35 36 37 38 39 40 41

  /**
   * The widget settings.
   *
   * @var array
   */
  protected $settings;

  /**
   * Constructs a WidgetBase object.
   *
   * @param array $plugin_id
   *   The plugin_id for the widget.
42
   * @param mixed $plugin_definition
43
   *   The plugin implementation definition.
44
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
45
   *   The definition of the field to which the widget is associated.
46 47
   * @param array $settings
   *   The widget settings.
48 49
   * @param array $third_party_settings
   *   Any third party settings settings.
50
   */
51
  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
52
    parent::__construct(array(), $plugin_id, $plugin_definition);
53
    $this->fieldDefinition = $field_definition;
54
    $this->settings = $settings;
55
    $this->thirdPartySettings = $third_party_settings;
56 57 58
  }

  /**
59
   * {@inheritdoc}
60
   */
61
  public function form(FieldItemListInterface $items, array &$form, array &$form_state, $get_delta = NULL) {
62
    $field_name = $this->fieldDefinition->getName();
63 64 65
    $parents = $form['#parents'];

    // Store field information in $form_state.
66
    if (!static::getWidgetState($parents, $field_name, $form_state)) {
67
      $field_state = array(
68 69
        'items_count' => count($items),
        'array_parents' => array(),
70
      );
71
      static::setWidgetState($parents, $field_name, $form_state, $field_state);
72 73 74 75 76 77 78 79
    }

    // Collect widget elements.
    $elements = array();

    // If the widget is handling multiple values (e.g Options), or if we are
    // displaying an individual element, just get a single form element and make
    // it the $delta value.
80
    if ($this->handlesMultipleValues() || isset($get_delta)) {
81 82
      $delta = isset($get_delta) ? $get_delta : 0;
      $element = array(
83
        '#title' => String::checkPlain($this->fieldDefinition->getLabel()),
84
        '#description' => field_filter_xss(\Drupal::token()->replace($this->fieldDefinition->getDescription())),
85
      );
86
      $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

      if ($element) {
        if (isset($get_delta)) {
          // If we are processing a specific delta value for a field where the
          // field module handles multiples, set the delta in the result.
          $elements[$delta] = $element;
        }
        else {
          // For fields that handle their own processing, we cannot make
          // assumptions about how the field is structured, just merge in the
          // returned element.
          $elements = $element;
        }
      }
    }
    // If the widget does not handle multiple values itself, (and we are not
    // displaying an individual element), process the multiple value form.
    else {
105
      $elements = $this->formMultipleElements($items, $form, $form_state);
106 107 108
    }

    // Populate the 'array_parents' information in $form_state['field'] after
109 110 111
    // the form is built, so that we catch changes in the form structure
    // performed in alter() hooks.
    $elements['#after_build'][] = array(get_class($this), 'afterBuild');
112 113
    $elements['#field_name'] = $field_name;
    $elements['#field_parents'] = $parents;
114 115 116 117 118
    // Enforce the structure of submitted values.
    $elements['#parents'] = array_merge($parents, array($field_name));
    // Most widgets need their internal structure preserved in submitted values.
    $elements += array('#tree' => TRUE);

119 120 121 122 123 124 125 126 127 128
    return array(
      // Aid in theming of widgets by rendering a classified container.
      '#type' => 'container',
      // Assign a different parent, to keep the main id for the widget itself.
      '#parents' => array_merge($parents, array($field_name . '_wrapper')),
      '#attributes' => array(
        'class' => array(
          'field-type-' . drupal_html_class($this->fieldDefinition->getType()),
          'field-name-' . drupal_html_class($field_name),
          'field-widget-' . drupal_html_class($this->getPluginId()),
129 130
        ),
      ),
131
      'widget' => $elements,
132 133 134 135 136 137 138 139 140 141 142
    );
  }

  /**
   * Special handling to create form elements for multiple values.
   *
   * Handles generic features for multiple fields:
   * - number of widgets
   * - AHAH-'add more' button
   * - table display and drag-n-drop value reordering
   */
143
  protected function formMultipleElements(FieldItemListInterface $items, array &$form, array &$form_state) {
144
    $field_name = $this->fieldDefinition->getName();
145
    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
146 147 148
    $parents = $form['#parents'];

    // Determine the number of widgets to display.
149
    switch ($cardinality) {
150
      case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
151
        $field_state = static::getWidgetState($parents, $field_name, $form_state);
152 153 154 155 156
        $max = $field_state['items_count'];
        $is_multiple = TRUE;
        break;

      default:
157 158
        $max = $cardinality - 1;
        $is_multiple = ($cardinality > 1);
159 160 161
        break;
    }

162
    $title = String::checkPlain($this->fieldDefinition->getLabel());
163
    $description = field_filter_xss(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
164 165 166 167 168 169 170 171 172 173

    $elements = array();

    for ($delta = 0; $delta <= $max; $delta++) {
      // For multiple fields, title and description are handled by the wrapping
      // table.
      $element = array(
        '#title' => $is_multiple ? '' : $title,
        '#description' => $is_multiple ? '' : $description,
      );
174
      $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
175 176 177 178 179 180 181 182 183 184 185 186

      if ($element) {
        // Input field for the delta (drag-n-drop reordering).
        if ($is_multiple) {
          // We name the element '_weight' to avoid clashing with elements
          // defined by widget.
          $element['_weight'] = array(
            '#type' => 'weight',
            '#title' => t('Weight for row @number', array('@number' => $delta + 1)),
            '#title_display' => 'invisible',
            // Note: this 'delta' is the FAPI #type 'weight' element's property.
            '#delta' => $max,
187
            '#default_value' => $items[$delta]->_weight ?: $delta,
188 189 190 191 192 193 194 195 196 197 198
            '#weight' => 100,
          );
        }

        $elements[$delta] = $element;
      }
    }

    if ($elements) {
      $elements += array(
        '#theme' => 'field_multiple_value_form',
199 200
        '#field_name' => $field_name,
        '#cardinality' => $cardinality,
201
        '#cardinality_multiple' => $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(),
202
        '#required' => $this->fieldDefinition->isRequired(),
203 204 205 206 207 208
        '#title' => $title,
        '#description' => $description,
        '#max_delta' => $max,
      );

      // Add 'add more' button, if not working with a programmed form.
209
      if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && empty($form_state['programmed'])) {
210 211 212 213 214 215

        $id_prefix = implode('-', array_merge($parents, array($field_name)));
        $wrapper_id = drupal_html_id($id_prefix . '-add-more-wrapper');
        $elements['#prefix'] = '<div id="' . $wrapper_id . '">';
        $elements['#suffix'] = '</div>';

216 217 218 219 220
        $elements['add_more'] = array(
          '#type' => 'submit',
          '#name' => strtr($id_prefix, '-', '_') . '_add_more',
          '#value' => t('Add another item'),
          '#attributes' => array('class' => array('field-add-more-submit')),
221
          '#limit_validation_errors' => array(array_merge($parents, array($field_name))),
222
          '#submit' => array(array(get_class($this), 'addMoreSubmit')),
223
          '#ajax' => array(
224 225 226
            'callback' => array(get_class($this), 'addMoreAjax'),
            'wrapper' => $wrapper_id,
            'effect' => 'fade',
227 228 229 230 231 232 233 234
          ),
        );
      }
    }

    return $elements;
  }

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
  /**
   * After-build handler for field elements in a form.
   *
   * This stores the final location of the field within the form structure so
   * that flagErrors() can assign validation errors to the right form element.
   */
  public static function afterBuild(array $element, array &$form_state) {
    $parents = $element['#field_parents'];
    $field_name = $element['#field_name'];

    $field_state = static::getWidgetState($parents, $field_name, $form_state);
    $field_state['array_parents'] = $element['#array_parents'];
    static::setWidgetState($parents, $field_name, $form_state, $field_state);

    return $element;
  }

252 253 254 255 256 257 258 259 260 261 262 263
  /**
   * Submission handler for the "Add another item" button.
   */
  public static function addMoreSubmit(array $form, array &$form_state) {
    $button = $form_state['triggering_element'];

    // Go one level up in the form, to the widgets container.
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
    $field_name = $element['#field_name'];
    $parents = $element['#field_parents'];

    // Increment the items count.
264
    $field_state = static::getWidgetState($parents, $field_name, $form_state);
265
    $field_state['items_count']++;
266
    static::setWidgetState($parents, $field_name, $form_state, $field_state);
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283

    $form_state['rebuild'] = TRUE;
  }

  /**
   * Ajax callback for the "Add another item" button.
   *
   * This returns the new page content to replace the page content made obsolete
   * by the form submission.
   */
  public static function addMoreAjax(array $form, array $form_state) {
    $button = $form_state['triggering_element'];

    // Go one level up in the form, to the widgets container.
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));

    // Ensure the widget allows adding additional items.
284
    if ($element['#cardinality'] != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
285 286 287 288 289 290 291 292 293 294 295
      return;
    }

    // Add a DIV around the delta receiving the Ajax effect.
    $delta = $element['#max_delta'];
    $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
    $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';

    return $element;
  }

296 297 298
  /**
   * Generates the form element for a single copy of the widget.
   */
299
  protected function formSingleElement(FieldItemListInterface $items, $delta, array $element, array &$form, array &$form_state) {
300 301
    $entity = $items->getEntity();

302 303 304
    $element += array(
      '#field_parents' => $form['#parents'],
      // Only the first widget should be required.
305
      '#required' => $delta == 0 && $this->fieldDefinition->isRequired(),
306 307 308 309
      '#delta' => $delta,
      '#weight' => $delta,
    );

310
    $element = $this->formElement($items, $delta, $element, $form, $form_state);
311 312 313 314 315

    if ($element) {
      // Allow modules to alter the field widget form element.
      $context = array(
        'form' => $form,
316
        'widget' => $this,
317 318 319 320
        'items' => $items,
        'delta' => $delta,
        'default' => !empty($entity->field_ui_default_value),
      );
321
      \Drupal::moduleHandler()->alter(array('field_widget_form', 'field_widget_' . $this->getPluginId() . '_form'), $element, $form_state, $context);
322 323 324 325 326 327
    }

    return $element;
  }

  /**
328
   * {@inheritdoc}
329
   */
330
  public function extractFormValues(FieldItemListInterface $items, array $form, array &$form_state) {
331
    $field_name = $this->fieldDefinition->getName();
332 333

    // Extract the values from $form_state['values'].
334
    $path = array_merge($form['#parents'], array($field_name));
335
    $key_exists = NULL;
336
    $values = NestedArray::getValue($form_state['values'], $path, $key_exists);
337 338

    if ($key_exists) {
339 340 341 342 343 344 345 346 347
      // Account for drag-and-drop reordering if needed.
      if (!$this->handlesMultipleValues()) {
        // Remove the 'value' of the 'add more' button.
        unset($values['add_more']);

        // The original delta, before drag-and-drop reordering, is needed to
        // route errors to the corect form element.
        foreach ($values as $delta => &$value) {
          $value['_original_delta'] = $delta;
348 349
        }

350 351 352
        usort($values, function ($a, $b) {
          return SortArray::sortByKeyInt($a, $b, '_weight');
        });
353 354
      }

355 356
      // Let the widget massage the submitted values.
      $values = $this->massageFormValues($values, $form, $form_state);
357

358 359
      // Assign the values and remove the empty ones.
      $items->setValue($values);
360
      $items->filterEmptyItems();
361 362

      // Put delta mapping in $form_state, so that flagErrors() can use it.
363
      $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
364
      foreach ($items as $delta => $item) {
365 366
        $field_state['original_deltas'][$delta] = isset($item->_original_delta) ? $item->_original_delta : $delta;
        unset($item->_original_delta, $item->_weight);
367
      }
368
      static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
369 370 371 372
    }
  }

  /**
373
   * {@inheritdoc}
374
   */
375
  public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, array &$form_state) {
376
    $field_name = $this->fieldDefinition->getName();
377

378
    $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
379

380
    if ($violations->count()) {
381 382
      $form_builder = \Drupal::formBuilder();

383
      // Locate the correct element in the the form.
384
      $element = NestedArray::getValue($form_state['complete_form'], $field_state['array_parents']);
385

386 387 388 389 390 391 392 393 394 395 396 397 398
      // Do not report entity-level validation errors if Form API errors have
      // already been reported for the field.
      // @todo Field validation should not be run on fields with FAPI errors to
      //   begin with. See https://drupal.org/node/2070429.
      $element_path = implode('][', $element['#parents']);
      if ($reported_errors = $form_builder->getErrors($form_state)) {
        foreach (array_keys($reported_errors) as $error_path) {
          if (strpos($error_path, $element_path) === 0) {
            return;
          }
        }
      }

399 400
      // Only set errors if the element is accessible.
      if (!isset($element['#access']) || $element['#access']) {
401
        $handles_multiple = $this->handlesMultipleValues();
402

403
        $violations_by_delta = array();
404
        foreach ($violations as $violation) {
405 406 407
          // Separate violations by delta.
          $property_path = explode('.', $violation->getPropertyPath());
          $delta = array_shift($property_path);
408 409 410 411
          // Violations at the ItemList level are not associated to any delta,
          // we file them under $delta NULL.
          $delta = is_numeric($delta) ? $delta : NULL;

412
          $violations_by_delta[$delta][] = $violation;
413
          $violation->arrayPropertyPath = $property_path;
414 415
        }

416
        /** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $delta_violations */
417
        foreach ($violations_by_delta as $delta => $delta_violations) {
418 419 420
          // Pass violations to the main element:
          // - if this is a multiple-value widget,
          // - or if the violations are at the ItemList level.
421
          if ($handles_multiple || $delta === NULL) {
422 423
            $delta_element = $element;
          }
424
          // Otherwise, pass errors by delta to the corresponding sub-element.
425 426 427 428
          else {
            $original_delta = $field_state['original_deltas'][$delta];
            $delta_element = $element[$original_delta];
          }
429 430 431
          foreach ($delta_violations as $violation) {
            // @todo: Pass $violation->arrayPropertyPath as property path.
            $error_element = $this->errorElement($delta_element, $violation, $form, $form_state);
432
            if ($error_element !== FALSE) {
433
              $form_builder->setError($error_element, $form_state, $violation->getMessage());
434
            }
435 436 437 438 439 440
          }
        }
      }
    }
  }

441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
  /**
   * {@inheritdoc}
   */
  public static function getWidgetState(array $parents, $field_name, array &$form_state) {
    return NestedArray::getValue($form_state, static::getWidgetStateParents($parents, $field_name));
  }

  /**
   * {@inheritdoc}
   */
  public static function setWidgetState(array $parents, $field_name, array &$form_state, array $field_state) {
    NestedArray::setValue($form_state, static::getWidgetStateParents($parents, $field_name), $field_state);
  }

  /**
   * Returns the location of processing information within $form_state.
   *
   * @param array $parents
   *   The array of #parents where the widget lives in the form.
   * @param string $field_name
   *   The field name.
   *
   * @return array
   *   The location of processing information within $form_state.
   */
  protected static function getWidgetStateParents(array $parents, $field_name) {
    // Field processing data is placed at
    // $form_state['field']['#parents'][...$parents...]['#fields'][$field_name],
    // to avoid clashes between field names and $parents parts.
    return array_merge(array('field', '#parents'), $parents, array('#fields', $field_name));
  }

473
  /**
474
   * {@inheritdoc}
475 476 477 478 479
   */
  public function settingsForm(array $form, array &$form_state) {
    return array();
  }

480 481 482 483 484 485 486
  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    return array();
  }

487
  /**
488
   * {@inheritdoc}
489
   */
490
  public function errorElement(array $element, ConstraintViolationInterface $error, array $form, array &$form_state) {
491 492 493 494
    return $element;
  }

  /**
495
   * {@inheritdoc}
496 497 498 499 500
   */
  public function massageFormValues(array $values, array $form, array &$form_state) {
    return $values;
  }

501 502 503 504 505 506 507
  /**
   * Returns the array of field settings.
   *
   * @return array
   *   The array of settings.
   */
  protected function getFieldSettings() {
508
    return $this->fieldDefinition->getSettings();
509 510 511 512 513 514 515 516 517 518 519 520
  }

  /**
   * Returns the value of a field setting.
   *
   * @param string $setting_name
   *   The setting name.
   *
   * @return mixed
   *   The setting value.
   */
  protected function getFieldSetting($setting_name) {
521
    return $this->fieldDefinition->getSetting($setting_name);
522 523
  }

524 525 526 527 528 529 530 531 532 533 534 535
  /**
   * Returns whether the widget handles multiple values.
   *
   * @return bool
   *   TRUE if a single copy of formElement() can handle multiple field values,
   *   FALSE if multiple values require separate copies of formElement().
   */
  protected function handlesMultipleValues() {
    $definition = $this->getPluginDefinition();
    return $definition['multiple_values'];
  }

536
}