--- /dev/null
+<?php
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a form element for a set of checkboxes.
+ *
+ * Properties:
+ * - #options: An associative array whose keys are the values returned for each
+ * checkbox, and whose values are the labels next to each checkbox. The
+ * #options array cannot have a 0 key, as it would not be possible to discern
+ * checked and unchecked states.
+ *
+ * Usage example:
+ * @code
+ * $form['high_school']['tests_taken'] = array(
+ * '#type' => 'checkboxes',
+ * '#options' => array('SAT' => $this->t('SAT'), 'ACT' => $this->t('ACT')),
+ * '#title' => $this->t('What standardized tests did you take?'),
+ * ...
+ * );
+ * @endcode
+ *
+ * @see \Drupal\Core\Render\Element\Radios
+ * @see \Drupal\Core\Render\Element\Checkbox
+ *
+ * @FormElement("checkboxes")
+ */
+class Checkboxes extends FormElement {
+
+ use CompositeFormElementTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInfo() {
+ $class = get_class($this);
+ return [
+ '#input' => TRUE,
+ '#process' => [
+ [$class, 'processCheckboxes'],
+ ],
+ '#pre_render' => [
+ [$class, 'preRenderCompositeFormElement'],
+ ],
+ '#theme_wrappers' => ['checkboxes'],
+ ];
+ }
+
+ /**
+ * Processes a checkboxes form element.
+ */
+ public static function processCheckboxes(&$element, FormStateInterface $form_state, &$complete_form) {
+ $value = is_array($element['#value']) ? $element['#value'] : [];
+ $element['#tree'] = TRUE;
+ if (count($element['#options']) > 0) {
+ if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
+ $element['#default_value'] = [];
+ }
+ $weight = 0;
+ foreach ($element['#options'] as $key => $choice) {
+ // Integer 0 is not a valid #return_value, so use '0' instead.
+ // @see \Drupal\Core\Render\Element\Checkbox::valueCallback().
+ // @todo For Drupal 8, cast all integer keys to strings for consistency
+ // with \Drupal\Core\Render\Element\Radios::processRadios().
+ if ($key === 0) {
+ $key = '0';
+ }
+ // Maintain order of options as defined in #options, in case the element
+ // defines custom option sub-elements, but does not define all option
+ // sub-elements.
+ $weight += 0.001;
+
+ $element += [$key => []];
+ $element[$key] += [
+ '#type' => 'checkbox',
+ '#title' => $choice,
+ '#return_value' => $key,
+ '#default_value' => isset($value[$key]) ? $key : NULL,
+ '#attributes' => $element['#attributes'],
+ '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
+ // Errors should only be shown on the parent checkboxes element.
+ '#error_no_message' => TRUE,
+ '#weight' => $weight,
+ ];
+ }
+ }
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+ if ($input === FALSE) {
+ $value = [];
+ $element += ['#default_value' => []];
+ foreach ($element['#default_value'] as $key) {
+ $value[$key] = $key;
+ }
+ return $value;
+ }
+ elseif (is_array($input)) {
+ // Programmatic form submissions use NULL to indicate that a checkbox
+ // should be unchecked. We therefore remove all NULL elements from the
+ // array before constructing the return value, to simulate the behavior
+ // of web browsers (which do not send unchecked checkboxes to the server
+ // at all). This will not affect non-programmatic form submissions, since
+ // all values in \Drupal::request()->request are strings.
+ // @see \Drupal\Core\Form\FormBuilderInterface::submitForm()
+ foreach ($input as $key => $value) {
+ if (!isset($value)) {
+ unset($input[$key]);
+ }
+ }
+ return array_combine($input, $input);
+ }
+ else {
+ return [];
+ }
+ }
+
+ /**
+ * Determines which checkboxes were checked when a form is submitted.
+ *
+ * @param array $input
+ * An array returned by the FormAPI for a set of checkboxes.
+ *
+ * @return array
+ * An array of keys that were checked.
+ */
+ public static function getCheckedCheckboxes(array $input) {
+ // Browsers do not include unchecked options in a form submission. The
+ // FormAPI tries to normalize this to keep checkboxes consistent with other
+ // form elements. Checkboxes show up as an array in the form of option_id =>
+ // option_id|0, where integer 0 is an unchecked option.
+ //
+ // @see \Drupal\Core\Render\Element\Checkboxes::valueCallback()
+ // @see https://www.w3.org/TR/html401/interact/forms.html#checkbox
+ $checked = array_filter($input, function($value) {
+ return $value !== 0;
+ });
+ return array_keys($checked);
+ }
+
+ /**
+ * Determines if all checkboxes in a set are unchecked.
+ *
+ * @param array $input
+ * An array returned by the FormAPI for a set of checkboxes.
+ *
+ * @return bool
+ * TRUE if all options are unchecked. FALSE otherwise.
+ */
+ public static function detectEmptyCheckboxes(array $input) {
+ return empty(static::getCheckedCheckboxes($input));
+ }
+
+}