You are here

class WebformLikert in Webform 8.5

Same name in this branch
  1. 8.5 src/Element/WebformLikert.php \Drupal\webform\Element\WebformLikert
  2. 8.5 src/Plugin/WebformElement/WebformLikert.php \Drupal\webform\Plugin\WebformElement\WebformLikert
Same name and namespace in other branches
  1. 6.x src/Element/WebformLikert.php \Drupal\webform\Element\WebformLikert

Provides a webform element for a likert scale.

Plugin annotation

@FormElement("webform_likert");

Hierarchy

Expanded class hierarchy of WebformLikert

1 file declares its use of WebformLikert
WebformLikert.php in src/Plugin/WebformElement/WebformLikert.php
2 #type uses of WebformLikert
WebformExampleCustomFormSettingsForm::buildForm in modules/webform_example_custom_form/src/Form/WebformExampleCustomFormSettingsForm.php
Form constructor.
webform_test_test_options in tests/modules/webform_test/includes/webform_test.test_options.inc
Generate test options.

File

src/Element/WebformLikert.php, line 18

Namespace

Drupal\webform\Element
View source
class WebformLikert extends FormElement {

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    $class = get_class($this);
    return [
      '#input' => TRUE,
      '#process' => [
        [
          $class,
          'processWebformLikert',
        ],
        [
          $class,
          'processAjaxForm',
        ],
      ],
      '#theme_wrappers' => [
        'form_element',
      ],
      '#required' => FALSE,
      '#required_error' => '',
      '#sticky' => TRUE,
      '#questions' => [],
      '#questions_description_display' => 'description',
      // Using #answers instead of #options to prevent triggering
      // \Drupal\Core\Form\FormValidator::performRequiredValidation().
      '#answers' => [],
      '#answers_description_display' => 'description',
      '#na_answer' => FALSE,
      '#na_answer_text' => '',
      '#na_answer_value' => '',
    ];
  }

  /**
   * Processes a likert scale webform element.
   */
  public static function processWebformLikert(&$element, FormStateInterface $form_state, &$complete_form) {

    // Get answer with optional N/A.
    static::processWebformLikertAnswers($element);

    // Remove 'for' from element's label.
    $element['#label_attributes']['webform-remove-for-attribute'] = TRUE;

    // Process answers.
    $answers = [];
    foreach ($element['#answers'] as $answer_key => $answer) {
      $answer = (string) $answer;
      if (!WebformOptionsHelper::hasOptionDescription($answer)) {
        $answer_description_property_name = NULL;
        $answer_title = $answer;
        $answer_description = '';
      }
      else {
        $answer_description_property_name = $element['#answers_description_display'] === 'help' ? 'help' : 'description';
        list($answer_title, $answer_description) = WebformOptionsHelper::splitOption($answer);
      }
      $answers[$answer_key] = [
        'description_property_name' => $answer_description_property_name,
        'title' => $answer_title,
        'description' => $answer_description,
      ];
    }

    // Build header.
    $header = [
      'likert_question' => [
        'data' => [
          'title' => WebformAccessibilityHelper::buildVisuallyHidden(t('Questions')),
        ],
      ],
    ];
    foreach ($answers as $answer_key => $answer) {
      $header[$answer_key] = [
        'data' => [
          'title' => [
            '#markup' => $answer['title'],
          ],
        ],
      ];
      switch ($answer['description_property_name']) {
        case 'help':
          $header[$answer_key]['data']['help'] = [
            '#type' => 'webform_help',
            '#help' => $answer['description'],
            '#help_title' => $answer['title'],
          ];
          break;
        case 'description':
          $header[$answer_key]['data']['description'] = [
            '#type' => 'container',
            '#markup' => $answer['description'],
            '#attributes' => [
              'class' => [
                'description',
              ],
            ],
          ];
          break;
      }
    }

    // Randomize questions.
    if (!empty($element['#questions_randomize'])) {
      $element['#questions'] = WebformElementHelper::randomize($element['#questions']);
    }

    // Build rows.
    $rows = [];
    foreach ($element['#questions'] as $question_key => $question) {
      $question = (string) $question;
      if (!WebformOptionsHelper::hasOptionDescription($question)) {
        $question_description_property_name = NULL;
        $question_title = $question;
        $question_description = '';
      }
      else {
        $question_description_property_name = $element['#questions_description_display'] === 'help' ? '#help' : '#description';
        list($question_title, $question_description) = WebformOptionsHelper::splitOption($question);
      }
      $value = isset($element['#value'][$question_key]) ? $element['#value'][$question_key] : NULL;

      // Get question id.
      // @see \Drupal\Core\Form\FormBuilder::doBuildForm
      $question_id = 'edit-' . implode('-', array_merge($element['#parents'], [
        'table',
        $question_key,
        'likert_question',
      ]));
      $question_id = Html::getUniqueId($question_id);
      $row = [];

      // Must format the label as an item so that inline webform errors will be
      // displayed.
      $row['likert_question'] = [
        '#type' => 'item',
        '#title' => $question_title,
        '#id' => $question_id,
        // Must include an empty <span> so that the item's value is
        // not required.
        '#value' => '<span></span>',
        '#webform_element' => TRUE,
        '#required' => $element['#required'],
        '#label_attributes' => [
          'webform-remove-for-attribute' => TRUE,
        ],
      ];
      if ($question_description_property_name) {
        $row['likert_question'][$question_description_property_name] = $question_description;
      }
      foreach ($answers as $answer_key => $answer) {
        $answer_attributes = [
          'aria-labelledby' => $question_id,
        ];

        // Add required attributes to input without setting the <label>
        // to required.
        if ($element['#required']) {
          $answer_attributes['required'] = 'required';
          $answer_attributes['aria-required'] = 'true';
        }
        $row[$answer_key] = [
          '#parents' => [
            $element['#name'],
            $question_key,
          ],
          '#type' => 'radio',
          // Must cast values as strings to prevent NULL and empty strings.
          // from being evaluated as 0.
          '#return_value' => (string) $answer_key,
          // Set value to FALSE to prevent '0' or '' from being checked when
          // value is NULL.
          // @see \Drupal\Core\Render\Element\Radio::preRenderRadio
          '#value' => $value === NULL ? FALSE : (string) $value,
          '#attributes' => $answer_attributes,
        ];

        // Wrap title in span.webform-likert-label.visually-hidden
        // so that it can hidden but accessible to screen readers
        // when Likert is displayed in grid on desktop.
        // Wrap help and description in
        // span.webform-likert-(help|description).hidden to block screen
        // readers except on mobile.
        // @see webform.element.likert.css
        $row[$answer_key]['#title_display'] = 'after';
        switch ($answer['description_property_name']) {
          case 'help':
            $build = [
              'title' => [
                '#markup' => $answer['title'],
              ],
              'help' => [
                '#type' => 'webform_help',
                '#help' => $answer['description'],
                '#help_title' => $answer['title'],
                '#prefix' => '<span class="webform-likert-help hidden">',
                '#suffix' => '</span>',
              ],
              '#prefix' => '<span class="webform-likert-label visually-hidden">',
              '#suffix' => '</span>',
            ];
            $row[$answer_key]['#title'] = \Drupal::service('renderer')
              ->render($build);
            break;
          case 'description':
            $row[$answer_key] += [
              '#title' => new FormattableMarkup('<span class="webform-likert-label visually-hidden">@title</span>', [
                '@title' => $answer['title'],
              ]),
              '#description' => new FormattableMarkup('<span class="webform-likert-description hidden">@description</span>', [
                '@description' => $answer['description'],
              ]),
            ];
            break;
          default:
            $row[$answer_key] += [
              '#title' => new FormattableMarkup('<span class="webform-likert-label visually-hidden">@title</span>', [
                '@title' => $answer['title'],
              ]),
            ];
        }
      }
      $rows[$question_key] = $row;
    }
    $element['table'] = [
      '#type' => 'table',
      '#header' => $header,
      '#sticky' => $element['#sticky'],
      '#attributes' => [
        'class' => [
          'webform-likert-table',
        ],
        'data-likert-answers-count' => count($element['#answers']),
      ],
      '#prefix' => '<div class="webform-likert-table-wrapper">',
      '#suffix' => '</div>',
    ] + $rows;

    // Build table element with selected properties.
    $properties = [
      '#states',
      '#sticky',
    ];
    $element['table'] += array_intersect_key($element, array_combine($properties, $properties));
    $element['#tree'] = TRUE;

    // Add after_build callback.
    $element['#after_build'][] = [
      get_called_class(),
      'afterBuild',
    ];

    // Add validate callback.
    $element += [
      '#element_validate' => [],
    ];
    array_unshift($element['#element_validate'], [
      get_called_class(),
      'validateWebformLikert',
    ]);
    $element['#attached']['library'][] = 'webform/webform.element.likert';
    return $element;
  }

  /**
   * Get likert element's answer which can include an N/A option.
   *
   * @param array $element
   *   The element.
   */
  public static function processWebformLikertAnswers(array &$element) {
    if (empty($element['#na_answer']) || empty($element['#answers'])) {
      return;
    }
    $na_value = !empty($element['#na_answer_value']) ? $element['#na_answer_value'] : (string) t('N/A');
    $na_text = !empty($element['#na_answer_text']) ? $element['#na_answer_text'] : $na_value;
    $element['#answers'] += [
      $na_value => $na_text,
    ];
  }

  /**
   * Performs the after_build callback.
   */
  public static function afterBuild(array $element, FormStateInterface $form_state) {
    if ($form_state
      ->isProcessingInput()) {

      // Likert elements contain a table which uses 'item' form elements to
      // display the questions. These 'item' elements provide undesired data to
      // the $form_state values. Set the value in $form_state again to overwrite
      // the undesired item values.
      // @see https://www.drupal.org/project/webform/issues/3090007
      $form_state
        ->setValueForElement($element, $element['#value']);
    }
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
    $default_value = [];
    foreach ($element['#questions'] as $question_key => $question_title) {
      $default_value[$question_key] = NULL;
    }
    if ($input === FALSE) {

      // FormBuilder can provide a default #default_value of an empty string.
      if (empty($element['#default_value'])) {
        $element['#default_value'] = [];
      }
      return $element['#default_value'] + $default_value;
    }
    $value = $default_value;
    foreach ($value as $allowed_key => $default) {
      if (isset($input[$allowed_key]) && is_scalar($input[$allowed_key])) {
        $value[$allowed_key] = (string) $input[$allowed_key];
      }
    }
    return $value;
  }

  /**
   * Validates a likert element.
   */
  public static function validateWebformLikert(&$element, FormStateInterface $form_state, &$complete_form) {
    if (!empty($element['#required'])) {
      static::setRequiredError($element, $form_state);
    }
  }

  /**
   * Set element required error messages.
   *
   * @param array $element
   *   An element.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current form's state.
   */
  public static function setRequiredError(array &$element, FormStateInterface $form_state) {
    $value = $element['#value'];
    foreach ($element['#questions'] as $question_key => $question_title) {
      if (is_null($value[$question_key])) {
        $question_element =& $element['table'][$question_key]['likert_question'];
        $t_args = [
          '@name' => $question_title,
        ];
        if (!empty($element['#required_error'])) {
          $form_state
            ->setError($question_element, new FormattableMarkup($element['#required_error'], $t_args));
        }
        else {
          $form_state
            ->setError($question_element, t('@name field is required.', $t_args));
        }
      }
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
FormElement::processAutocomplete public static function Adds autocomplete functionality to elements.
FormElement::processPattern public static function #process callback for #pattern form element property.
FormElement::validatePattern public static function #element_validate callback for #pattern form element property.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 3
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginBase::__construct public function Constructs a \Drupal\Component\Plugin\PluginBase object. 92
RenderElement::preRenderAjaxForm public static function Adds Ajax information about an element to communicate with JavaScript.
RenderElement::preRenderGroup public static function Adds members of this group as actual elements for rendering.
RenderElement::processAjaxForm public static function Form element processing handler for the #ajax form property. 1
RenderElement::processGroup public static function Arranges elements into groups.
RenderElement::setAttributes public static function Sets a form element's class attribute. Overrides ElementInterface::setAttributes
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
WebformLikert::afterBuild public static function Performs the after_build callback.
WebformLikert::getInfo public function Returns the element properties for this element. Overrides ElementInterface::getInfo
WebformLikert::processWebformLikert public static function Processes a likert scale webform element.
WebformLikert::processWebformLikertAnswers public static function Get likert element's answer which can include an N/A option.
WebformLikert::setRequiredError public static function Set element required error messages.
WebformLikert::validateWebformLikert public static function Validates a likert element.
WebformLikert::valueCallback public static function Determines how user input is mapped to an element's #value property. Overrides FormElement::valueCallback