'submit', * '#value' => t('Submit'), * ); * return $form; * } * function my_module_example_form_validate($form, &$form_state) { * // Validation logic. * } * function my_module_example_form_submit($form, &$form_state) { * // Submission logic. * } * @endcode * * Or with any number of additional arguments: * @code * $extra = "extra"; * $form = drupal_get_form('my_module_example_form', $extra); * ... * function my_module_example_form($form, &$form_state, $extra) { * $form['submit'] = array( * '#type' => 'submit', * '#value' => $extra, * ); * return $form; * } * @endcode * * The $form argument to form-related functions is a structured array containing * the elements and properties of the form. For information on the array * components and format, and more detailed explanations of the Form API * workflow, see the * @link forms_api_reference.html Form API reference @endlink * and the * @link http://drupal.org/node/37775 Form API documentation section. @endlink * In addition, there is a set of Form API tutorials in * @link form_example_tutorial.inc the Form Example Tutorial @endlink which * provide basics all the way up through multistep forms. * * In the form builder, validation, submission, and other form functions, * $form_state is the primary influence on the processing of the form and is * passed by reference to most functions, so they use it to communicate with * the form system and each other. * * See drupal_build_form() for documentation of $form_state keys. */ /** * Returns a renderable form array for a given form ID. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->getForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::getForm(). */ function drupal_get_form($form_arg) { return call_user_func_array(array(\Drupal::formBuilder(), 'getForm'), func_get_args()); } /** * Builds and processes a form for a given form ID. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->buildForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::buildForm(). */ function drupal_build_form($form_id, &$form_state) { return \Drupal::formBuilder()->buildForm($form_id, $form_state); } /** * Retrieves default values for the $form_state array. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->getFormStateDefaults(). * * @see \Drupal\Core\Form\FormBuilderInterface::getFormStateDefaults(). */ function form_state_defaults() { return \Drupal::formBuilder()->getFormStateDefaults(); } /** * Constructs a new $form from the information in $form_state. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->rebuildForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::rebuildForm(). */ function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) { return \Drupal::formBuilder()->rebuildForm($form_id, $form_state, $old_form); } /** * Fetches a form from the cache. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->getCache(). * * @see \Drupal\Core\Form\FormBuilderInterface::getCache(). */ function form_get_cache($form_build_id, &$form_state) { return \Drupal::formBuilder()->getCache($form_build_id, $form_state); } /** * Stores a form in the cache. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->setCache(). * * @see \Drupal\Core\Form\FormBuilderInterface::setCache(). */ function form_set_cache($form_build_id, $form, $form_state) { \Drupal::formBuilder()->setCache($form_build_id, $form, $form_state); } /** * Ensures an include file is loaded whenever the form is processed. * * Example: * @code * // Load node.admin.inc from Node module. * form_load_include($form_state, 'inc', 'node', 'node.admin'); * @endcode * * Use this function instead of module_load_include() from inside a form * constructor or any form processing logic as it ensures that the include file * is loaded whenever the form is processed. In contrast to using * module_load_include() directly, form_load_include() makes sure the include * file is correctly loaded also if the form is cached. * * @param $form_state * The current state of the form. * @param $type * The include file's type (file extension). * @param $module * The module to which the include file belongs. * @param $name * (optional) The base file name (without the $type extension). If omitted, * $module is used; i.e., resulting in "$module.$type" by default. * * @return * The filepath of the loaded include file, or FALSE if the include file was * not found or has been loaded already. * * @see module_load_include() */ function form_load_include(&$form_state, $type, $module, $name = NULL) { if (!isset($name)) { $name = $module; } if (!isset($form_state['build_info']['files']["$module:$name.$type"])) { // Only add successfully included files to the form state. if ($result = module_load_include($type, $module, $name)) { $form_state['build_info']['files']["$module:$name.$type"] = array( 'type' => $type, 'module' => $module, 'name' => $name, ); return $result; } } return FALSE; } /** * Retrieves, populates, and processes a form. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->submitForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::submitForm(). */ function drupal_form_submit($form_arg, &$form_state) { \Drupal::formBuilder()->submitForm($form_arg, $form_state); } /** * Retrieves the structured array that defines a given form. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->retrieveForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::retrieveForm(). */ function drupal_retrieve_form($form_id, &$form_state) { return \Drupal::formBuilder()->retrieveForm($form_id, $form_state); } /** * Processes a form submission. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->processForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::processForm(). */ function drupal_process_form($form_id, &$form, &$form_state) { \Drupal::formBuilder()->processForm($form_id, $form, $form_state); } /** * Prepares a structured form array. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->prepareForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::prepareForm(). */ function drupal_prepare_form($form_id, &$form, &$form_state) { \Drupal::formBuilder()->prepareForm($form_id, $form, $form_state); } /** * Validates user-submitted form data in the $form_state array. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->validateForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::validateForm(). */ function drupal_validate_form($form_id, &$form, &$form_state) { \Drupal::formBuilder()->validateForm($form_id, $form, $form_state); } /** * Redirects the user to a URL after a form has been processed. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->redirectForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::redirectForm(). */ function drupal_redirect_form($form_state) { return \Drupal::formBuilder()->redirectForm($form_state); } /** * Executes custom validation and submission handlers for a given form. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->executeHandlers(). * * @see \Drupal\Core\Form\FormBuilderInterface::executeHandlers(). */ function form_execute_handlers($type, &$form, &$form_state) { \Drupal::formBuilder()->executeHandlers($type, $form, $form_state); } /** * Files an error against a form element. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->setErrorByName(). * * @see \Drupal\Core\Form\FormBuilderInterface::setErrorByName(). */ function form_set_error($name, array &$form_state, $message = '') { \Drupal::formBuilder()->setErrorByName($name, $form_state, $message); } /** * Clears all errors against all form elements made by form_set_error(). * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->clearErrors(). * * @see \Drupal\Core\Form\FormBuilderInterface::clearErrors(). */ function form_clear_error(array &$form_state) { \Drupal::formBuilder()->clearErrors($form_state); } /** * Returns an associative array of all errors. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->getErrors(). * * @see \Drupal\Core\Form\FormBuilderInterface::getErrors(). */ function form_get_errors(array &$form_state) { return \Drupal::formBuilder()->getErrors($form_state); } /** * Returns the error message filed against the given form element. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->getError(). * * @see \Drupal\Core\Form\FormBuilderInterface::getError(). */ function form_get_error($element, array &$form_state) { return \Drupal::formBuilder()->getError($element, $form_state); } /** * Flags an element as having an error. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->setError(). * * @see \Drupal\Core\Form\FormBuilderInterface::setError(). */ function form_error(&$element, array &$form_state, $message = '') { \Drupal::formBuilder()->setError($element, $form_state, $message); } /** * Builds and processes all elements in the structured form array. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->doBuildForm(). * * @see \Drupal\Core\Form\FormBuilderInterface::doBuildForm(). */ function form_builder($form_id, &$element, &$form_state) { return \Drupal::formBuilder()->doBuildForm($form_id, $element, $form_state); } /** * Removes internal Form API elements and buttons from submitted form values. * * This function can be used when a module wants to store all submitted form * values, for example, by serializing them into a single database column. In * such cases, all internal Form API values and all form button elements should * not be contained, and this function allows to remove them before the module * proceeds to storage. Next to button elements, the following internal values * are removed: * - form_id * - form_token * - form_build_id * - op * * @param $form_state * A keyed array containing the current state of the form, including * submitted form values; altered by reference. */ function form_state_values_clean(&$form_state) { // Remove internal Form API values. unset($form_state['values']['form_id'], $form_state['values']['form_token'], $form_state['values']['form_build_id'], $form_state['values']['op']); // Remove button values. // form_builder() collects all button elements in a form. We remove the button // value separately for each button element. foreach ($form_state['buttons'] as $button) { // Remove this button's value from the submitted form values by finding // the value corresponding to this button. // We iterate over the #parents of this button and move a reference to // each parent in $form_state['values']. For example, if #parents is: // array('foo', 'bar', 'baz') // then the corresponding $form_state['values'] part will look like this: // array( // 'foo' => array( // 'bar' => array( // 'baz' => 'button_value', // ), // ), // ) // We start by (re)moving 'baz' to $last_parent, so we are able unset it // at the end of the iteration. Initially, $values will contain a // reference to $form_state['values'], but in the iteration we move the // reference to $form_state['values']['foo'], and finally to // $form_state['values']['foo']['bar'], which is the level where we can // unset 'baz' (that is stored in $last_parent). $parents = $button['#parents']; $last_parent = array_pop($parents); $key_exists = NULL; $values = &NestedArray::getValue($form_state['values'], $parents, $key_exists); if ($key_exists && is_array($values)) { unset($values[$last_parent]); } } } /** * Determines the value for an image button form element. * * @param $form * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * @param $form_state * A keyed array containing the current state of the form. * * @return * The data that will appear in the $form_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_image_button_value($form, $input, $form_state) { if ($input !== FALSE) { if (!empty($input)) { // If we're dealing with Mozilla or Opera, we're lucky. It will // return a proper value, and we can get on with things. return $form['#return_value']; } else { // Unfortunately, in IE we never get back a proper value for THIS // form element. Instead, we get back two split values: one for the // X and one for the Y coordinates on which the user clicked the // button. We'll find this element in the #post data, and search // in the same spot for its name, with '_x'. $input = $form_state['input']; foreach (explode('[', $form['#name']) as $element_name) { // chop off the ] that may exist. if (substr($element_name, -1) == ']') { $element_name = substr($element_name, 0, -1); } if (!isset($input[$element_name])) { if (isset($input[$element_name . '_x'])) { return $form['#return_value']; } return NULL; } $input = $input[$element_name]; } return $form['#return_value']; } } } /** * Determines the value for a checkbox form element. * * @param $form * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * * @return * The data that will appear in the $element_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_checkbox_value($element, $input = FALSE) { if ($input === FALSE) { // Use #default_value as the default value of a checkbox, except change // NULL to 0, because FormBuilder::handleInputElement() would otherwise // replace NULL with empty string, but an empty string is a potentially // valid value for a checked checkbox. return isset($element['#default_value']) ? $element['#default_value'] : 0; } else { // Checked checkboxes are submitted with a value (possibly '0' or ''): // http://www.w3.org/TR/html401/interact/forms.html#successful-controls. // For checked checkboxes, browsers submit the string version of // #return_value, but we return the original #return_value. For unchecked // checkboxes, browsers submit nothing at all, but // FormBuilder::handleInputElement() detects this, and calls this // function with $input=NULL. Returning NULL from a value callback means to // use the default value, which is not what is wanted when an unchecked // checkbox is submitted, so we use integer 0 as the value indicating an // unchecked checkbox. Therefore, modules must not use integer 0 as a // #return_value, as doing so results in the checkbox always being treated // as unchecked. The string '0' is allowed for #return_value. The most // common use-case for setting #return_value to either 0 or '0' is for the // first option within a 0-indexed array of checkboxes, and for this, // form_process_checkboxes() uses the string rather than the integer. return isset($input) ? $element['#return_value'] : 0; } } /** * Determines the value for a checkboxes form element. * * @param $element * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * * @return * The data that will appear in the $element_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_checkboxes_value($element, $input = FALSE) { if ($input === FALSE) { $value = array(); $element += array('#default_value' => array()); 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; see drupal_form_submit(). 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. foreach ($input as $key => $value) { if (!isset($value)) { unset($input[$key]); } } return array_combine($input, $input); } else { return array(); } } /** * Determines the value of a table form element. * * @param array $element * The form element whose value is being populated. * @param array|false $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * * @return array * The data that will appear in the $form_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_table_value(array $element, $input = FALSE) { // If #multiple is FALSE, the regular default value of radio buttons is used. if (!empty($element['#tableselect']) && !empty($element['#multiple'])) { // Contrary to #type 'checkboxes', the default value of checkboxes in a // table is built from the array keys (instead of array values) of the // #default_value property. // @todo D8: Remove this inconsistency. if ($input === FALSE) { $element += array('#default_value' => array()); $value = array_keys(array_filter($element['#default_value'])); return array_combine($value, $value); } else { return is_array($input) ? array_combine($input, $input) : array(); } } } /** * Form value callback: Determines the value for a #type radios form element. * * @param $element * The form element whose value is being populated. * @param $input * (optional) The incoming input to populate the form element. If FALSE, the * element's default value is returned. Defaults to FALSE. * * @return * The data that will appear in the $element_state['values'] collection for * this element. */ function form_type_radios_value(&$element, $input = FALSE) { if ($input !== FALSE) { // When there's user input (including NULL), return it as the value. // However, if NULL is submitted, FormBuilder::handleInputElement() will // apply the default value, and we want that validated against #options // unless it's empty. (An empty #default_value, such as NULL or FALSE, can // be used to indicate that no radio button is selected by default.) if (!isset($input) && !empty($element['#default_value'])) { $element['#needs_validation'] = TRUE; } return $input; } else { // For default value handling, simply return #default_value. Additionally, // for a NULL default value, set #has_garbage_value to prevent // FormBuilder::handleInputElement() converting the NULL to an empty // string, so that code can distinguish between nothing selected and the // selection of a radio button whose value is an empty string. $value = isset($element['#default_value']) ? $element['#default_value'] : NULL; if (!isset($value)) { $element['#has_garbage_value'] = TRUE; } return $value; } } /** * Determines the value for a tableselect form element. * * @param $element * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * * @return * The data that will appear in the $element_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_tableselect_value($element, $input = FALSE) { // If $element['#multiple'] == FALSE, then radio buttons are displayed and // the default value handling is used. if (isset($element['#multiple']) && $element['#multiple']) { // Checkboxes are being displayed with the default value coming from the // keys of the #default_value property. This differs from the checkboxes // element which uses the array values. if ($input === FALSE) { $value = array(); $element += array('#default_value' => array()); foreach ($element['#default_value'] as $key => $flag) { if ($flag) { $value[$key] = $key; } } return $value; } else { return is_array($input) ? array_combine($input, $input) : array(); } } } /** * Determines the value for a password_confirm form element. * * @param $element * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * * @return * The data that will appear in the $element_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_password_confirm_value($element, $input = FALSE) { if ($input === FALSE) { $element += array('#default_value' => array()); return $element['#default_value'] + array('pass1' => '', 'pass2' => ''); } } /** * Determines the value for a select form element. * * @param $element * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * * @return * The data that will appear in the $element_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_select_value($element, $input = FALSE) { if ($input !== FALSE) { if (isset($element['#multiple']) && $element['#multiple']) { // If an enabled multi-select submits NULL, it means all items are // unselected. A disabled multi-select always submits NULL, and the // default value should be used. if (empty($element['#disabled'])) { return (is_array($input)) ? array_combine($input, $input) : array(); } else { return (isset($element['#default_value']) && is_array($element['#default_value'])) ? $element['#default_value'] : array(); } } // Non-multiple select elements may have an empty option preprended to them // (see form_process_select()). When this occurs, usually #empty_value is // an empty string, but some forms set #empty_value to integer 0 or some // other non-string constant. PHP receives all submitted form input as // strings, but if the empty option is selected, set the value to match the // empty value exactly. elseif (isset($element['#empty_value']) && $input === (string) $element['#empty_value']) { return $element['#empty_value']; } else { return $input; } } } /** * Determines the value for a textfield form element. * * @param $element * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * * @return * The data that will appear in the $element_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_textfield_value($element, $input = FALSE) { if ($input !== FALSE && $input !== NULL) { // Equate $input to the form value to ensure it's marked for // validation. return str_replace(array("\r", "\n"), '', $input); } } /** * Determines the value for form's token value. * * @param $element * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, * the element's default value should be returned. * * @return * The data that will appear in the $element_state['values'] collection * for this element. Return nothing to use the default. */ function form_type_token_value($element, $input = FALSE) { if ($input !== FALSE) { return (string) $input; } } /** * Changes submitted form values during form validation. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->setValue(). * * @see \Drupal\Core\Form\FormBuilderInterface::setValue(). */ function form_set_value($element, $value, &$form_state) { \Drupal::formBuilder()->setValue($element, $value, $form_state); } /** * Allows PHP array processing of multiple select options with the same value. * * Used for form select elements which need to validate HTML option groups * and multiple options which may return the same value. Associative PHP arrays * cannot handle these structures, since they share a common key. * * @param $array * The form options array to process. * * @return * An array with all hierarchical elements flattened to a single array. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * Use \Drupal::formBuilder()->flattenOptions(). * * @see \Drupal\Core\Form\FormBuilderInterface::flattenOptions(). */ function form_options_flatten($array) { return \Drupal::formBuilder()->flattenOptions($array); } /** * Processes a select list form element. * * This process callback is mandatory for select fields, since all user agents * automatically preselect the first available option of single (non-multiple) * select lists. * * @param $element * The form element to process. Properties used: * - #multiple: (optional) Indicates whether one or more options can be * selected. Defaults to FALSE. * - #default_value: Must be NULL or not set in case there is no value for the * element yet, in which case a first default option is inserted by default. * Whether this first option is a valid option depends on whether the field * is #required or not. * - #required: (optional) Whether the user needs to select an option (TRUE) * or not (FALSE). Defaults to FALSE. * - #empty_option: (optional) The label to show for the first default option. * By default, the label is automatically set to "- Please select -" for a * required field and "- None -" for an optional field. * - #empty_value: (optional) The value for the first default option, which is * used to determine whether the user submitted a value or not. * - If #required is TRUE, this defaults to '' (an empty string). * - If #required is not TRUE and this value isn't set, then no extra option * is added to the select control, leaving the control in a slightly * illogical state, because there's no way for the user to select nothing, * since all user agents automatically preselect the first available * option. But people are used to this being the behavior of select * controls. * @todo Address the above issue in Drupal 8. * - If #required is not TRUE and this value is set (most commonly to an * empty string), then an extra option (see #empty_option above) * representing a "non-selection" is added with this as its value. * * @see _form_validate() */ function form_process_select($element) { // #multiple select fields need a special #name. if ($element['#multiple']) { $element['#attributes']['multiple'] = 'multiple'; $element['#attributes']['name'] = $element['#name'] . '[]'; } // A non-#multiple select needs special handling to prevent user agents from // preselecting the first option without intention. #multiple select lists do // not get an empty option, as it would not make sense, user interface-wise. else { // If the element is set to #required through #states, override the // element's #required setting. $required = isset($element['#states']['required']) ? TRUE : $element['#required']; // If the element is required and there is no #default_value, then add an // empty option that will fail validation, so that the user is required to // make a choice. Also, if there's a value for #empty_value or // #empty_option, then add an option that represents emptiness. if (($required && !isset($element['#default_value'])) || isset($element['#empty_value']) || isset($element['#empty_option'])) { $element += array( '#empty_value' => '', '#empty_option' => $required ? t('- Select -') : t('- None -'), ); // The empty option is prepended to #options and purposively not merged // to prevent another option in #options mistakenly using the same value // as #empty_value. $empty_option = array($element['#empty_value'] => $element['#empty_option']); $element['#options'] = $empty_option + $element['#options']; } } return $element; } /** * Prepares variables for select element templates. * * Default template: select.html.twig. * * It is possible to group options together; to do this, change the format of * $options to an associative array in which the keys are group labels, and the * values are associative arrays in the normal $options format. * * @param $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #title, #value, #options, #description, #extra, * #multiple, #required, #name, #attributes, #size. */ function template_preprocess_select(&$variables) { $element = $variables['element']; element_set_attributes($element, array('id', 'name', 'size')); _form_set_attributes($element, array('form-select')); $variables['attributes'] = $element['#attributes']; $variables['options'] = form_select_options($element); } /** * Converts a select form element's options array into HTML. * * @param $element * An associative array containing the properties of the element. * @param $choices * Mixed: Either an associative array of items to list as choices, or an * object with an 'option' member that is an associative array. This * parameter is only used internally and should not be passed. * * @return * An HTML string of options for the select form element. */ function form_select_options($element, $choices = NULL) { if (!isset($choices)) { if (empty($element['#options'])) { return ''; } $choices = $element['#options']; } // array_key_exists() accommodates the rare event where $element['#value'] is NULL. // isset() fails in this situation. $value_valid = isset($element['#value']) || array_key_exists('#value', $element); $value_is_array = $value_valid && is_array($element['#value']); // Check if the element is multiple select and no value has been selected. $empty_value = (empty($element['#value']) && !empty($element['#multiple'])); $options = ''; foreach ($choices as $key => $choice) { if (is_array($choice)) { $options .= ''; $options .= form_select_options($element, $choice); $options .= ''; } elseif (is_object($choice)) { $options .= form_select_options($element, $choice->option); } else { $key = (string) $key; $empty_choice = $empty_value && $key == '_none'; if ($value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice)) { $selected = ' selected="selected"'; } else { $selected = ''; } $options .= ''; } } return $options; } /** * Returns the indexes of a select element's options matching a given key. * * This function is useful if you need to modify the options that are * already in a form element; for example, to remove choices which are * not valid because of additional filters imposed by another module. * One example might be altering the choices in a taxonomy selector. * To correctly handle the case of a multiple hierarchy taxonomy, * #options arrays can now hold an array of objects, instead of a * direct mapping of keys to labels, so that multiple choices in the * selector can have the same key (and label). This makes it difficult * to manipulate directly, which is why this helper function exists. * * This function does not support optgroups (when the elements of the * #options array are themselves arrays), and will return FALSE if * arrays are found. The caller must either flatten/restore or * manually do their manipulations in this case, since returning the * index is not sufficient, and supporting this would make the * "helper" too complicated and cumbersome to be of any help. * * As usual with functions that can return array() or FALSE, do not * forget to use === and !== if needed. * * @param $element * The select element to search. * @param $key * The key to look for. * * @return * An array of indexes that match the given $key. Array will be * empty if no elements were found. FALSE if optgroups were found. */ function form_get_options($element, $key) { $keys = array(); foreach ($element['#options'] as $index => $choice) { if (is_array($choice)) { return FALSE; } elseif (is_object($choice)) { if (isset($choice->option[$key])) { $keys[] = $index; } } elseif ($index == $key) { $keys[] = $index; } } return $keys; } /** * Prepares variables for fieldset element templates. * * Default template: fieldset.html.twig. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #attributes, #children, #description, #id, #title, * #value. */ function template_preprocess_fieldset(&$variables) { $element = $variables['element']; element_set_attributes($element, array('id')); _form_set_attributes($element, array('form-wrapper')); $variables['attributes'] = $element['#attributes']; $variables['attributes']['class'][] = 'form-item'; // If the element is required, a required marker is appended to the label. $variables['required'] = ''; if (!empty($element['#required'])) { $variables['required'] = array( '#theme' => 'form_required_marker', '#element' => $element, ); } $variables['prefix'] = isset($element['#field_prefix']) ? $element['#field_prefix'] : NULL; $variables['suffix'] = isset($element['#field_suffix']) ? $element['#field_suffix'] : NULL; $variables['children'] = $element['#children']; // Build legend properties. $variables['legend'] = array(); $legend_attributes = array(); if (isset($element['#title_display']) && $element['#title_display'] == 'invisible') { $legend_attributes['class'][] = 'visually-hidden'; } $variables['legend']['attributes'] = new Attribute($legend_attributes); $variables['legend']['title'] = (isset($element['#title']) && $element['#title'] !== '') ? Xss::filterAdmin($element['#title']) : ''; // Build description properties. $variables['description'] = array(); if (!empty($element['#description'])) { $description_id = $element['#attributes']['id'] . '--description'; $description_attributes = array( 'class' => 'description', 'id' => $description_id, ); $variables['description']['attributes'] = new Attribute($description_attributes); $variables['description']['content'] = $element['#description']; // Add the description's id to the fieldset aria attributes. $variables['attributes']['aria-describedby'] = $description_id; } } /** * Prepares variables for details element templates. * * Default template: details.html.twig. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #attributes, #children, #open, * #description, #id, #title, #value, #optional. * * @ingroup themeable */ function template_preprocess_details(&$variables) { $element = $variables['element']; $variables['attributes'] = $element['#attributes']; $variables['summary_attributes'] = new Attribute(); if (!empty($element['#title'])) { $variables['summary_attributes']['role'] = 'button'; if (!empty($element['#attributes']['id'])) { $variables['summary_attributes']['aria-controls'] = $element['#attributes']['id']; } $variables['summary_attributes']['aria-expanded'] = !empty($element['#attributes']['open']); $variables['summary_attributes']['aria-pressed'] = $variables['summary_attributes']['aria-expanded']; } $variables['title'] = (!empty($element['#title'])) ? $element['#title'] : ''; $variables['description'] = (!empty($element['#description'])) ? $element['#description'] : ''; $variables['children'] = (isset($element['#children'])) ? $element['#children'] : ''; $variables['value'] = (isset($element['#value'])) ? $element['#value'] : ''; } /** * Prepares a #type 'radio' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #required, #return_value, #value, #attributes, #title, * #description. * * Note: The input "name" attribute needs to be sanitized before output, which * is currently done by initializing Drupal\Core\Template\Attribute with * all the attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_radio($element) { $element['#attributes']['type'] = 'radio'; element_set_attributes($element, array('id', 'name', '#return_value' => 'value')); if (isset($element['#return_value']) && $element['#value'] !== FALSE && $element['#value'] == $element['#return_value']) { $element['#attributes']['checked'] = 'checked'; } _form_set_attributes($element, array('form-radio')); return $element; } /** * Prepares variables for radios templates. * * Default template: radios.html.twig. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #title, #value, #options, #description, #required, * #attributes, #children. */ function template_preprocess_radios(&$variables) { $element = $variables['element']; $variables['attributes'] = array(); if (isset($element['#id'])) { $variables['attributes']['id'] = $element['#id']; } $variables['attributes']['class'][] = 'form-radios'; if (!empty($element['#attributes']['class'])) { $variables['attributes']['class'] = array_merge($variables['attributes']['class'], $element['#attributes']['class']); } if (isset($element['#attributes']['title'])) { $variables['attributes']['title'] = $element['#attributes']['title']; } $variables['children'] = $element['#children']; } /** * Expand a password_confirm field into two text boxes. */ function form_process_password_confirm($element) { $element['pass1'] = array( '#type' => 'password', '#title' => t('Password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'], '#required' => $element['#required'], '#attributes' => array('class' => array('password-field')), ); $element['pass2'] = array( '#type' => 'password', '#title' => t('Confirm password'), '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], '#required' => $element['#required'], '#attributes' => array('class' => array('password-confirm')), ); $element['#element_validate'] = array('password_confirm_validate'); $element['#tree'] = TRUE; if (isset($element['#size'])) { $element['pass1']['#size'] = $element['pass2']['#size'] = $element['#size']; } return $element; } /** * Validates a password_confirm element. */ function password_confirm_validate($element, &$element_state) { $pass1 = trim($element['pass1']['#value']); $pass2 = trim($element['pass2']['#value']); if (!empty($pass1) || !empty($pass2)) { if (strcmp($pass1, $pass2)) { form_error($element, $element_state, t('The specified passwords do not match.')); } } elseif ($element['#required'] && !empty($element_state['input'])) { form_error($element, $element_state, t('Password field is required.')); } // Password field must be converted from a two-element array into a single // string regardless of validation results. form_set_value($element['pass1'], NULL, $element_state); form_set_value($element['pass2'], NULL, $element_state); form_set_value($element, $pass1, $element_state); return $element; } /** * Returns HTML for an #date form element. * * Supports HTML5 types of 'date', 'datetime', 'datetime-local', and 'time'. * Falls back to a plain textfield. Used as a sub-element by the datetime * element type. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #title, #value, #options, #description, #required, * #attributes, #id, #name, #type, #min, #max, #step, #value, #size. * * @ingroup themeable */ function theme_date($variables) { $element = $variables['element']; if (empty($element['attribute']['type'])) { $element['attribute']['type'] = 'date'; } element_set_attributes($element, array('id', 'name', 'type', 'min', 'max', 'step', 'value', 'size')); _form_set_attributes($element, array('form-' . $element['attribute']['type'])); return ''; } /** * Sets the value for a weight element, with zero as a default. */ function weight_value(&$form) { if (isset($form['#default_value'])) { $form['#value'] = $form['#default_value']; } else { $form['#value'] = 0; } } /** * Expands a radios element into individual radio elements. */ function form_process_radios($element) { if (count($element['#options']) > 0) { $weight = 0; foreach ($element['#options'] as $key => $choice) { // 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 += array($key => array()); // Generate the parents as the autogenerator does, so we will have a // unique id for each radio button. $parents_for_id = array_merge($element['#parents'], array($key)); $element[$key] += array( '#type' => 'radio', '#title' => $choice, // The key is sanitized in Drupal\Core\Template\Attribute during output // from the theme function. '#return_value' => $key, // Use default or FALSE. A value of FALSE means that the radio button is // not 'checked'. '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : FALSE, '#attributes' => $element['#attributes'], '#parents' => $element['#parents'], '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, '#weight' => $weight, ); } } return $element; } /** * Prepares a #type 'checkbox' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #return_value, #description, #required, * #attributes, #checked. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_checkbox($element) { $element['#attributes']['type'] = 'checkbox'; element_set_attributes($element, array('id', 'name', '#return_value' => 'value')); // Unchecked checkbox has #value of integer 0. if (!empty($element['#checked'])) { $element['#attributes']['checked'] = 'checked'; } _form_set_attributes($element, array('form-checkbox')); return $element; } /** * Returns HTML for a set of checkbox form elements. * * @param $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #children, #attributes. * * @ingroup themeable */ function theme_checkboxes($variables) { $element = $variables['element']; $attributes = array(); if (isset($element['#id'])) { $attributes['id'] = $element['#id']; } $attributes['class'][] = 'form-checkboxes'; if (!empty($element['#attributes']['class'])) { $attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']); } if (isset($element['#attributes']['title'])) { $attributes['title'] = $element['#attributes']['title']; } return '' . (!empty($element['#children']) ? $element['#children'] : '') . ''; } /** * Adds form element theming to an element if its title or description is set. * * This is used as a pre render function for checkboxes and radios. */ function form_pre_render_conditional_form_element($element) { // Set the element's title attribute to show #title as a tooltip, if needed. if (isset($element['#title']) && $element['#title_display'] == 'attribute') { $element['#attributes']['title'] = $element['#title']; if (!empty($element['#required'])) { // Append an indication that this field is required. $element['#attributes']['title'] .= ' (' . t('Required') . ')'; } } if (isset($element['#title']) || isset($element['#description'])) { // @see #type 'fieldgroup' $element['#theme_wrappers'][] = 'fieldset'; $element['#attributes']['class'][] = 'fieldgroup'; $element['#attributes']['class'][] = 'form-composite'; } return $element; } /** * Processes a form button element. */ function form_process_button($element, $form_state) { // If this is a button intentionally allowing incomplete form submission // (e.g., a "Previous" or "Add another item" button), then also skip // client-side validation. if (isset($element['#limit_validation_errors']) && $element['#limit_validation_errors'] !== FALSE) { $element['#attributes']['formnovalidate'] = 'formnovalidate'; } return $element; } /** * Sets the #checked property of a checkbox element. */ function form_process_checkbox($element, $form_state) { $value = $element['#value']; $return_value = $element['#return_value']; // On form submission, the #value of an available and enabled checked // checkbox is #return_value, and the #value of an available and enabled // unchecked checkbox is integer 0. On not submitted forms, and for // checkboxes with #access=FALSE or #disabled=TRUE, the #value is // #default_value (integer 0 if #default_value is NULL). Most of the time, // a string comparison of #value and #return_value is sufficient for // determining the "checked" state, but a value of TRUE always means checked // (even if #return_value is 'foo'), and a value of FALSE or integer 0 always // means unchecked (even if #return_value is '' or '0'). if ($value === TRUE || $value === FALSE || $value === 0) { $element['#checked'] = (bool) $value; } else { // Compare as strings, so that 15 is not considered equal to '15foo', but 1 // is considered equal to '1'. This cast does not imply that either #value // or #return_value is expected to be a string. $element['#checked'] = ((string) $value === (string) $return_value); } return $element; } /** * Processes a checkboxes form element. */ function form_process_checkboxes($element) { $value = is_array($element['#value']) ? $element['#value'] : array(); $element['#tree'] = TRUE; if (count($element['#options']) > 0) { if (!isset($element['#default_value']) || $element['#default_value'] == 0) { $element['#default_value'] = array(); } $weight = 0; foreach ($element['#options'] as $key => $choice) { // Integer 0 is not a valid #return_value, so use '0' instead. // @see form_type_checkbox_value(). // @todo For Drupal 8, cast all integer keys to strings for consistency // with form_process_radios(). 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 += array($key => array()); $element[$key] += array( '#type' => 'checkbox', '#title' => $choice, '#return_value' => $key, '#default_value' => isset($value[$key]) ? $key : NULL, '#attributes' => $element['#attributes'], '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, '#weight' => $weight, ); } } return $element; } /** * Processes a form actions container element. * * @param $element * An associative array containing the properties and children of the * form actions container. * @param $form_state * The $form_state array for the form this element belongs to. * * @return * The processed element. */ function form_process_actions($element, &$form_state) { $element['#attributes']['class'][] = 'form-actions'; return $element; } /** * #pre_render callback for #type 'actions'. * * This callback iterates over all child elements of the #type 'actions' * container to look for elements with a #dropbutton property, so as to group * those elements into dropbuttons. As such, it works similar to #group, but is * specialized for dropbuttons. * * The value of #dropbutton denotes the dropbutton to group the child element * into. For example, two different values of 'foo' and 'bar' on child elements * would generate two separate dropbuttons, which each contain the corresponding * buttons. * * @param array $element * The #type 'actions' element to process. * * @return array * The processed #type 'actions' element, including individual buttons grouped * into new #type 'dropbutton' elements. */ function form_pre_render_actions_dropbutton(array $element) { $dropbuttons = array(); foreach (element_children($element, TRUE) as $key) { if (isset($element[$key]['#dropbutton'])) { $dropbutton = $element[$key]['#dropbutton']; // If there is no dropbutton for this button group yet, create one. if (!isset($dropbuttons[$dropbutton])) { $dropbuttons[$dropbutton] = array( '#type' => 'dropbutton', ); } // Add this button to the corresponding dropbutton. // @todo Change #type 'dropbutton' to be based on theme_item_list() // instead of links.html.twig to avoid this preemptive rendering. $button = drupal_render($element[$key]); $dropbuttons[$dropbutton]['#links'][$key] = array( 'title' => $button, 'html' => TRUE, ); } } // @todo For now, all dropbuttons appear first. Consider to invent a more // fancy sorting/injection algorithm here. return $dropbuttons + $element; } /** * #process callback for #pattern form element property. * * @param $element * An associative array containing the properties and children of the * generic input element. * @param $form_state * The $form_state array for the form this element belongs to. * * @return * The processed element. * * @see form_validate_pattern() */ function form_process_pattern($element, &$form_state) { if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) { $element['#attributes']['pattern'] = $element['#pattern']; $element['#element_validate'][] = 'form_validate_pattern'; } return $element; } /** * #element_validate callback for #pattern form element property. * * @param $element * An associative array containing the properties and children of the * generic form element. * @param $form_state * The $form_state array for the form this element belongs to. * * @see form_process_pattern() */ function form_validate_pattern($element, &$form_state) { if ($element['#value'] !== '') { // The pattern must match the entire string and should have the same // behavior as the RegExp object in ECMA 262. // - Use bracket-style delimiters to avoid introducing a special delimiter // character like '/' that would have to be escaped. // - Put in brackets so that the pattern can't interfere with what's // prepended and appended. $pattern = '{^(?:' . $element['#pattern'] . ')$}'; if (!preg_match($pattern, $element['#value'])) { form_error($element, $form_state, t('%name field is not in the right format.', array('%name' => $element['#title']))); } } } /** * Processes a container element. * * @param $element * An associative array containing the properties and children of the * container. * @param $form_state * The $form_state array for the form this element belongs to. * * @return * The processed element. */ function form_process_container($element, &$form_state) { // Generate the ID of the element if it's not explicitly given. if (!isset($element['#id'])) { $element['#id'] = drupal_html_id(implode('-', $element['#parents']) . '-wrapper'); } return $element; } /** * Returns HTML for a table with radio buttons or checkboxes. * * @param $variables * An associative array containing: * - element: An associative array containing the properties and children of * the tableselect element. Properties used: #header, #options, #empty, * and #js_select. The #options property is an array of selection options; * each array element of #options is an array of properties. These * properties can include #attributes, which is added to the * table row's HTML attributes; see theme_table(). An example of per-row * options: * @code * $options = array( * array( * 'title' => 'How to Learn Drupal', * 'content_type' => 'Article', * 'status' => 'published', * '#attributes' => array('class' => array('article-row')), * ), * array( * 'title' => 'Privacy Policy', * 'content_type' => 'Page', * 'status' => 'published', * '#attributes' => array('class' => array('page-row')), * ), * ); * $header = array( * 'title' => t('Title'), * 'content_type' => t('Content type'), * 'status' => t('Status'), * ); * $form['table'] = array( * '#type' => 'tableselect', * '#header' => $header, * '#options' => $options, * '#empty' => t('No content available.'), * ); * @endcode * * @ingroup themeable */ function theme_tableselect($variables) { $element = $variables['element']; $table = array( '#type' => 'table', ); $rows = array(); $header = $element['#header']; if (!empty($element['#options'])) { // Generate a table row for each selectable item in #options. foreach (element_children($element) as $key) { $row = array(); $row['data'] = array(); if (isset($element['#options'][$key]['#attributes'])) { $row += $element['#options'][$key]['#attributes']; } // Render the checkbox / radio element. $row['data'][] = drupal_render($element[$key]); // As theme_table only maps header and row columns by order, create the // correct order by iterating over the header fields. foreach ($element['#header'] as $fieldname => $title) { // A row cell can span over multiple headers, which means less row cells // than headers could be present. if (isset($element['#options'][$key][$fieldname])) { // A header can span over multiple cells and in this case the cells // are passed in an array. The order of this array determines the // order in which they are added. if (!isset($element['#options'][$key][$fieldname]['data']) && is_array($element['#options'][$key][$fieldname])) { foreach ($element['#options'][$key][$fieldname] as $cell) { $row['data'][] = $cell; } } else { $row['data'][] = $element['#options'][$key][$fieldname]; } } } $rows[] = $row; } // Add an empty header or a "Select all" checkbox to provide room for the // checkboxes/radios in the first table column. if ($element['#js_select']) { // Add a "Select all" checkbox. $table['#attached']['library'][] = 'core/drupal.tableselect'; array_unshift($header, array('class' => array('select-all'))); } else { // Add an empty header when radio buttons are displayed or a "Select all" // checkbox is not desired. array_unshift($header, ''); } } $table += array( '#header' => $header, '#rows' => $rows, '#empty' => $element['#empty'], '#attributes' => $element['#attributes'], ); return drupal_render($table); } /** * Creates checkbox or radio elements to populate a tableselect table. * * @param $element * An associative array containing the properties and children of the * tableselect element. * * @return * The processed element. */ function form_process_tableselect($element) { if ($element['#multiple']) { $value = is_array($element['#value']) ? $element['#value'] : array(); } else { // Advanced selection behavior makes no sense for radios. $element['#js_select'] = FALSE; } $element['#tree'] = TRUE; if (count($element['#options']) > 0) { if (!isset($element['#default_value']) || $element['#default_value'] === 0) { $element['#default_value'] = array(); } // Create a checkbox or radio for each item in #options in such a way that // the value of the tableselect element behaves as if it had been of type // checkboxes or radios. foreach ($element['#options'] as $key => $choice) { // Do not overwrite manually created children. if (!isset($element[$key])) { if ($element['#multiple']) { $title = ''; if (!empty($element['#options'][$key]['title']['data']['#title'])) { $title = t('Update @title', array( '@title' => $element['#options'][$key]['title']['data']['#title'], )); } $element[$key] = array( '#type' => 'checkbox', '#title' => $title, '#title_display' => 'invisible', '#return_value' => $key, '#default_value' => isset($value[$key]) ? $key : NULL, '#attributes' => $element['#attributes'], ); } else { // Generate the parents as the autogenerator does, so we will have a // unique id for each radio button. $parents_for_id = array_merge($element['#parents'], array($key)); $element[$key] = array( '#type' => 'radio', '#title' => '', '#return_value' => $key, '#default_value' => ($element['#default_value'] == $key) ? $key : NULL, '#attributes' => $element['#attributes'], '#parents' => $element['#parents'], '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, ); } if (isset($element['#options'][$key]['#weight'])) { $element[$key]['#weight'] = $element['#options'][$key]['#weight']; } } } } else { $element['#value'] = array(); } return $element; } /** * #process callback for #type 'table' to add tableselect support. * * @param array $element * An associative array containing the properties and children of the * table element. * @param array $form_state * The current state of the form. * * @return array * The processed element. * * @see form_process_tableselect() * @see theme_tableselect() */ function form_process_table($element, &$form_state) { if ($element['#tableselect']) { if ($element['#multiple']) { $value = is_array($element['#value']) ? $element['#value'] : array(); } // Advanced selection behavior makes no sense for radios. else { $element['#js_select'] = FALSE; } // Add a "Select all" checkbox column to the header. // @todo D8: Rename into #select_all? if ($element['#js_select']) { $element['#attached']['library'][] = 'core/drupal.tableselect'; array_unshift($element['#header'], array('class' => array('select-all'))); } // Add an empty header column for radio buttons or when a "Select all" // checkbox is not desired. else { array_unshift($element['#header'], ''); } if (!isset($element['#default_value']) || $element['#default_value'] === 0) { $element['#default_value'] = array(); } // Create a checkbox or radio for each row in a way that the value of the // tableselect element behaves as if it had been of #type checkboxes or // radios. foreach (element_children($element) as $key) { $row = &$element[$key]; // Prepare the element #parents for the tableselect form element. // Their values have to be located in child keys (#tree is ignored), since // form_validate_table() has to be able to validate whether input (for the // parent #type 'table' element) has been submitted. $element_parents = array_merge($element['#parents'], array($key)); // Since the #parents of the tableselect form element will equal the // #parents of the row element, prevent FormBuilder from auto-generating // an #id for the row element, since drupal_html_id() would automatically // append a suffix to the tableselect form element's #id otherwise. $row['#id'] = drupal_html_id('edit-' . implode('-', $element_parents) . '-row'); // Do not overwrite manually created children. if (!isset($row['select'])) { // Determine option label; either an assumed 'title' column, or the // first available column containing a #title or #markup. // @todo Consider to add an optional $element[$key]['#title_key'] // defaulting to 'title'? unset($label_element); $title = NULL; if (isset($row['title']['#type']) && $row['title']['#type'] == 'label') { $label_element = &$row['title']; } else { if (!empty($row['title']['#title'])) { $title = $row['title']['#title']; } else { foreach (element_children($row) as $column) { if (isset($row[$column]['#title'])) { $title = $row[$column]['#title']; break; } if (isset($row[$column]['#markup'])) { $title = $row[$column]['#markup']; break; } } } if (isset($title) && $title !== '') { $title = t('Update !title', array('!title' => $title)); } } // Prepend the select column to existing columns. $row = array('select' => array()) + $row; $row['select'] += array( '#type' => $element['#multiple'] ? 'checkbox' : 'radio', '#id' => drupal_html_id('edit-' . implode('-', $element_parents)), // @todo If rows happen to use numeric indexes instead of string keys, // this results in a first row with $key === 0, which is always FALSE. '#return_value' => $key, '#attributes' => $element['#attributes'], '#wrapper_attributes' => array( 'class' => array('table-select'), ), ); if ($element['#multiple']) { $row['select']['#default_value'] = isset($value[$key]) ? $key : NULL; $row['select']['#parents'] = $element_parents; } else { $row['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL); $row['select']['#parents'] = $element['#parents']; } if (isset($label_element)) { $label_element['#id'] = $row['select']['#id'] . '--label'; $label_element['#for'] = $row['select']['#id']; $row['select']['#attributes']['aria-labelledby'] = $label_element['#id']; $row['select']['#title_display'] = 'none'; } else { $row['select']['#title'] = $title; $row['select']['#title_display'] = 'invisible'; } } } } return $element; } /** * #element_validate callback for #type 'table'. * * @param array $element * An associative array containing the properties and children of the * table element. * @param array $form_state * The current state of the form. */ function form_validate_table($element, &$form_state) { // Skip this validation if the button to submit the form does not require // selected table row data. if (empty($form_state['triggering_element']['#tableselect'])) { return; } if ($element['#multiple']) { if (!is_array($element['#value']) || !count(array_filter($element['#value']))) { form_error($element, $form_state, t('No items selected.')); } } elseif (!isset($element['#value']) || $element['#value'] === '') { form_error($element, $form_state, t('No item selected.')); } } /** * Processes a machine-readable name form element. * * @param $element * The form element to process. Properties used: * - #machine_name: An associative array containing: * - exists: A callable to invoke for checking whether a submitted machine * name value already exists. The submitted value is passed as an * argument. In most cases, an existing API or menu argument loader * function can be re-used. The callback is only invoked, if the submitted * value differs from the element's #default_value. * - source: (optional) The #array_parents of the form element containing * the human-readable name (i.e., as contained in the $form structure) to * use as source for the machine name. Defaults to array('label'). * - label: (optional) A text to display as label for the machine name value * after the human-readable name form element. Defaults to "Machine name". * - replace_pattern: (optional) A regular expression (without delimiters) * matching disallowed characters in the machine name. Defaults to * '[^a-z0-9_]+'. * - replace: (optional) A character to replace disallowed characters in the * machine name via JavaScript. Defaults to '_' (underscore). When using a * different character, 'replace_pattern' needs to be set accordingly. * - error: (optional) A custom form error message string to show, if the * machine name contains disallowed characters. * - standalone: (optional) Whether the live preview should stay in its own * form element rather than in the suffix of the source element. Defaults * to FALSE. * - #maxlength: (optional) Should be set to the maximum allowed length of the * machine name. Defaults to 64. * - #disabled: (optional) Should be set to TRUE in case an existing machine * name must not be changed after initial creation. */ function form_process_machine_name($element, &$form_state) { // We need to pass the langcode to the client. $language = \Drupal::languageManager()->getCurrentLanguage(); // Apply default form element properties. $element += array( '#title' => t('Machine-readable name'), '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'), '#machine_name' => array(), '#field_prefix' => '', '#field_suffix' => '', '#suffix' => '', ); // A form element that only wants to set one #machine_name property (usually // 'source' only) would leave all other properties undefined, if the defaults // were defined in hook_element_info(). Therefore, we apply the defaults here. $element['#machine_name'] += array( 'source' => array('label'), 'target' => '#' . $element['#id'], 'label' => t('Machine name'), 'replace_pattern' => '[^a-z0-9_]+', 'replace' => '_', 'standalone' => FALSE, 'field_prefix' => $element['#field_prefix'], 'field_suffix' => $element['#field_suffix'], ); // By default, machine names are restricted to Latin alphanumeric characters. // So, default to LTR directionality. if (!isset($element['#attributes'])) { $element['#attributes'] = array(); } $element['#attributes'] += array('dir' => 'ltr'); // The source element defaults to array('name'), but may have been overidden. if (empty($element['#machine_name']['source'])) { return $element; } // Retrieve the form element containing the human-readable name from the // complete form in $form_state. By reference, because we may need to append // a #field_suffix that will hold the live preview. $key_exists = NULL; $source = NestedArray::getValue($form_state['complete_form'], $element['#machine_name']['source'], $key_exists); if (!$key_exists) { return $element; } $suffix_id = $source['#id'] . '-machine-name-suffix'; $element['#machine_name']['suffix'] = '#' . $suffix_id; if ($element['#machine_name']['standalone']) { $element['#suffix'] .= '  '; } else { // Append a field suffix to the source form element, which will contain // the live preview of the machine name. $source += array('#field_suffix' => ''); $source['#field_suffix'] .= '  '; $parents = array_merge($element['#machine_name']['source'], array('#field_suffix')); NestedArray::setValue($form_state['complete_form'], $parents, $source['#field_suffix']); } $js_settings = array( 'type' => 'setting', 'data' => array( 'machineName' => array( '#' . $source['#id'] => $element['#machine_name'], ), 'langcode' => $language->id, ), ); $element['#attached']['library'][] = 'core/drupal.machine-name'; $element['#attached']['js'][] = $js_settings; return $element; } /** * Form element validation handler for machine_name elements. * * Note that #maxlength is validated by _form_validate() already. */ function form_validate_machine_name(&$element, &$form_state) { // Verify that the machine name not only consists of replacement tokens. if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) { form_error($element, $form_state, t('The machine-readable name must contain unique characters.')); } // Verify that the machine name contains no disallowed characters. if (preg_match('@' . $element['#machine_name']['replace_pattern'] . '@', $element['#value'])) { if (!isset($element['#machine_name']['error'])) { // Since a hyphen is the most common alternative replacement character, // a corresponding validation error message is supported here. if ($element['#machine_name']['replace'] == '-') { form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.')); } // Otherwise, we assume the default (underscore). else { form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.')); } } else { form_error($element, $form_state, $element['#machine_name']['error']); } } // Verify that the machine name is unique. if ($element['#default_value'] !== $element['#value']) { $function = $element['#machine_name']['exists']; if (call_user_func($function, $element['#value'], $element, $form_state)) { form_error($element, $form_state, t('The machine-readable name is already in use. It must be unique.')); } } } /** * Arranges elements into groups. * * @param $element * An associative array containing the properties and children of the * element. Note that $element must be taken by reference here, so processed * child elements are taken over into $form_state. * @param $form_state * The $form_state array for the form this element belongs to. * * @return * The processed element. */ function form_process_group(&$element, &$form_state) { $parents = implode('][', $element['#parents']); // Each details element forms a new group. The #type 'vertical_tabs' basically // only injects a new details element. $form_state['groups'][$parents]['#group_exists'] = TRUE; $element['#groups'] = &$form_state['groups']; // Process vertical tabs group member details elements. if (isset($element['#group'])) { // Add this details element to the defined group (by reference). $group = $element['#group']; $form_state['groups'][$group][] = &$element; } return $element; } /** * Adds form element theming to details. * * @param $element * An associative array containing the properties and children of the * details. * * @return * The modified element. */ function form_pre_render_details($element) { element_set_attributes($element, array('id')); // The .form-wrapper class is required for #states to treat details like // containers. _form_set_attributes($element, array('form-wrapper')); // Collapsible details. $element['#attached']['library'][] = 'core/drupal.collapse'; if (!empty($element['#open'])) { $element['#attributes']['open'] = 'open'; } // Do not render optional details elements if there are no children. if (isset($element['#parents'])) { $group = implode('][', $element['#parents']); if (!empty($element['#optional']) && !element_get_visible_children($element['#groups'][$group])) { $element['#printed'] = TRUE; } } return $element; } /** * Adds members of this group as actual elements for rendering. * * @param $element * An associative array containing the properties and children of the * element. * * @return * The modified element with all group members. */ function form_pre_render_group($element) { // The element may be rendered outside of a Form API context. if (!isset($element['#parents']) || !isset($element['#groups'])) { return $element; } // Inject group member elements belonging to this group. $parents = implode('][', $element['#parents']); $children = element_children($element['#groups'][$parents]); if (!empty($children)) { foreach ($children as $key) { // Break references and indicate that the element should be rendered as // group member. $child = (array) $element['#groups'][$parents][$key]; $child['#group_details'] = TRUE; // Inject the element as new child element. $element[] = $child; $sort = TRUE; } // Re-sort the element's children if we injected group member elements. if (isset($sort)) { $element['#sorted'] = FALSE; } } if (isset($element['#group'])) { // Contains form element summary functionalities. $element['#attached']['library'][] = 'core/drupal.form'; $group = $element['#group']; // If this element belongs to a group, but the group-holding element does // not exist, we need to render it (at its original location). if (!isset($element['#groups'][$group]['#group_exists'])) { // Intentionally empty to clarify the flow; we simply return $element. } // If we injected this element into the group, then we want to render it. elseif (!empty($element['#group_details'])) { // Intentionally empty to clarify the flow; we simply return $element. } // Otherwise, this element belongs to a group and the group exists, so we do // not render it. elseif (element_children($element['#groups'][$group])) { $element['#printed'] = TRUE; } } return $element; } /** * Creates a group formatted as vertical tabs. * * @param $element * An associative array containing the properties and children of the * details element. * @param $form_state * The $form_state array for the form this vertical tab widget belongs to. * * @return * The processed element. */ function form_process_vertical_tabs($element, &$form_state) { // Inject a new details as child, so that form_process_details() processes // this details element like any other details. $element['group'] = array( '#type' => 'details', '#theme_wrappers' => array(), '#parents' => $element['#parents'], ); // Add an invisible label for accessibility. if (!isset($element['#title'])) { $element['#title'] = t('Vertical Tabs'); $element['#title_display'] = 'invisible'; } $element['#attached']['library'][] = 'core/drupal.vertical-tabs'; // The JavaScript stores the currently selected tab in this hidden // field so that the active tab can be restored the next time the // form is rendered, e.g. on preview pages or when form validation // fails. $name = implode('__', $element['#parents']); if (isset($form_state['values'][$name . '__active_tab'])) { $element['#default_tab'] = $form_state['values'][$name . '__active_tab']; } $element[$name . '__active_tab'] = array( '#type' => 'hidden', '#default_value' => $element['#default_tab'], '#attributes' => array('class' => array('vertical-tabs-active-tab')), ); return $element; } /** * Prepares a vertical_tabs element for rendering. * * @param array $element * An associative array containing the properties and children of the * vertical tabs element. * * @return array * The modified element. */ function form_pre_render_vertical_tabs($element) { // Do not render the vertical tabs element if it is empty. $group = implode('][', $element['#parents']); if (!element_get_visible_children($element['group']['#groups'][$group])) { $element['#printed'] = TRUE; } return $element; } /** * Prepares variables for vertical tabs templates. * * Default template: vertical-tabs.html.twig. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties and children of * the details element. Properties used: #children. * */ function template_preprocess_vertical_tabs(&$variables) { $element = $variables['element']; $variables['children'] = (!empty($element['#children'])) ? $element['#children'] : ''; } /** * Adds autocomplete functionality to elements with a valid * #autocomplete_route_name. * * Suppose your autocomplete route name is 'mymodule.autocomplete' and its path * is: '/mymodule/autocomplete/{a}/{b}' * In your form you have: * @code * '#autocomplete_route_name' => 'mymodule.autocomplete', * '#autocomplete_route_parameters' => array('a' => $some_key, 'b' => $some_id), * @endcode * The user types in "keywords" so the full path called is: * 'mymodule_autocomplete/$some_key/$some_id?q=keywords' * * @param array $element * The form element to process. Properties used: * - #autocomplete_route_name: A route to be used as callback URL by the * autocomplete JavaScript library. * - #autocomplete_route_parameters: The parameters to be used in conjunction * with the route name. * @param array $form_state * An associative array containing the current state of the form. * * @return array * The form element. */ function form_process_autocomplete($element, &$form_state) { $access = FALSE; if (!empty($element['#autocomplete_route_name'])) { $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array(); $path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters); $access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser()); } if ($access) { $element['#attributes']['class'][] = 'form-autocomplete'; $element['#attached']['library'][] = 'core/drupal.autocomplete'; // Provide a data attribute for the JavaScript behavior to bind to. $element['#attributes']['data-autocomplete-path'] = $path; } return $element; } /** * Preprocesses variables for theme_input(). * * @param array $variables * An associative array containing: * - element: An associative array containing the properties of the element. * * @ingroup themeable */ function template_preprocess_input(&$variables) { $element = $variables['element']; $variables['attributes'] = new Attribute($element['#attributes']); } /** * Returns HTML for an input form element. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #attributes. * * @ingroup themeable */ function theme_input($variables) { $element = $variables['element']; $attributes = $variables['attributes']; return '' . drupal_render_children($element); } /** * Prepares a #type 'button' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #attributes, #button_type, #name, #value. * * The #button_type property accepts any value, though core themes have CSS that * styles the following button_types appropriately: 'primary', 'danger'. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_button($element) { $element['#attributes']['type'] = 'submit'; element_set_attributes($element, array('id', 'name', 'value')); $element['#attributes']['class'][] = 'button'; if (!empty($element['#button_type'])) { $element['#attributes']['class'][] = 'button--' . $element['#button_type']; } // @todo Various JavaScript depends on this button class. $element['#attributes']['class'][] = 'form-submit'; if (!empty($element['#attributes']['disabled'])) { $element['#attributes']['class'][] = 'is-disabled'; } return $element; } /** * Prepares a #type 'image_button' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #attributes, #button_type, #name, #value, #title, #src. * * The #button_type property accepts any value, though core themes have css that * styles the following button_types appropriately: 'primary', 'danger'. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_image_button($element) { $element['#attributes']['type'] = 'image'; element_set_attributes($element, array('id', 'name', 'value')); $element['#attributes']['src'] = file_create_url($element['#src']); if (!empty($element['#title'])) { $element['#attributes']['alt'] = $element['#title']; $element['#attributes']['title'] = $element['#title']; } $element['#attributes']['class'][] = 'image-button'; if (!empty($element['#button_type'])) { $element['#attributes']['class'][] = 'image-button--' . $element['#button_type']; } // @todo Various JavaScript depends on this button class. $element['#attributes']['class'][] = 'form-submit'; if (!empty($element['#attributes']['disabled'])) { $element['#attributes']['class'][] = 'is-disabled'; } return $element; } /** * Prepares a #type 'hidden' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #name, #value, #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_hidden($element) { $element['#attributes']['type'] = 'hidden'; element_set_attributes($element, array('name', 'value')); return $element; } /** * Prepares a #type 'textfield' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #size, #maxlength, * #placeholder, #required, #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_textfield($element) { $element['#attributes']['type'] = 'text'; element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); _form_set_attributes($element, array('form-text')); return $element; } /** * Prepares a #type 'email' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #size, #maxlength, * #placeholder, #required, #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_email($element) { $element['#attributes']['type'] = 'email'; element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); _form_set_attributes($element, array('form-email')); return $element; } /** * Form element validation handler for #type 'email'. * * Note that #maxlength and #required is validated by _form_validate() already. */ function form_validate_email(&$element, &$form_state) { $value = trim($element['#value']); form_set_value($element, $value, $form_state); if ($value !== '' && !valid_email_address($value)) { form_error($element, $form_state, t('The e-mail address %mail is not valid.', array('%mail' => $value))); } } /** * Prepares a #type 'tel' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #size, #maxlength, * #placeholder, #required, #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_tel($element) { $element['#attributes']['type'] = 'tel'; element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); _form_set_attributes($element, array('form-tel')); return $element; } /** * Prepares a #type 'number' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #min, #max, #placeholder, * #required, #attributes, #step, #size. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_number($element) { $element['#attributes']['type'] = 'number'; element_set_attributes($element, array('id', 'name', 'value', 'step', 'min', 'max', 'placeholder', 'size')); _form_set_attributes($element, array('form-number')); return $element; } /** * Prepares a #type 'range' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #min, #max, #attributes, * #step. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_range($element) { $element['#attributes']['type'] = 'range'; element_set_attributes($element, array('id', 'name', 'value', 'step', 'min', 'max')); _form_set_attributes($element, array('form-range')); return $element; } /** * Form element validation handler for #type 'number'. * * Note that #required is validated by _form_validate() already. */ function form_validate_number(&$element, &$form_state) { $value = $element['#value']; if ($value === '') { return; } $name = empty($element['#title']) ? $element['#parents'][0] : $element['#title']; // Ensure the input is numeric. if (!is_numeric($value)) { form_error($element, $form_state, t('%name must be a number.', array('%name' => $name))); return; } // Ensure that the input is greater than the #min property, if set. if (isset($element['#min']) && $value < $element['#min']) { form_error($element, $form_state, t('%name must be higher than or equal to %min.', array('%name' => $name, '%min' => $element['#min']))); } // Ensure that the input is less than the #max property, if set. if (isset($element['#max']) && $value > $element['#max']) { form_error($element, $form_state, t('%name must be lower than or equal to %max.', array('%name' => $name, '%max' => $element['#max']))); } if (isset($element['#step']) && strtolower($element['#step']) != 'any') { // Check that the input is an allowed multiple of #step (offset by #min if // #min is set). $offset = isset($element['#min']) ? $element['#min'] : 0.0; if (!Number::validStep($value, $element['#step'], $offset)) { form_error($element, $form_state, t('%name is not a valid number.', array('%name' => $name))); } } } /** * Determines the value for a range element. * * Make sure range elements always have a value. The 'required' attribute is not * allowed for range elements. * * @param $element * The form element whose value is being populated. * @param $input * The incoming input to populate the form element. If this is FALSE, the * element's default value should be returned. * * @return * The data that will appear in the $form_state['values'] collection for * this element. Return nothing to use the default. */ function form_type_range_value($element, $input = FALSE) { if ($input === '') { $offset = ($element['#max'] - $element['#min']) / 2; // Round to the step. if (strtolower($element['#step']) != 'any') { $steps = round($offset / $element['#step']); $offset = $element['#step'] * $steps; } return $element['#min'] + $offset; } } /** * Prepares a #type 'url' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #size, #maxlength, * #placeholder, #required, #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_url($element) { $element['#attributes']['type'] = 'url'; element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); _form_set_attributes($element, array('form-url')); return $element; } /** * Prepares a #type 'search' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #size, #maxlength, * #placeholder, #required, #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_search($element) { $element['#attributes']['type'] = 'search'; element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); _form_set_attributes($element, array('form-search')); return $element; } /** * Form element validation handler for #type 'url'. * * Note that #maxlength and #required is validated by _form_validate() already. */ function form_validate_url(&$element, &$form_state) { $value = trim($element['#value']); form_set_value($element, $value, $form_state); if ($value !== '' && !valid_url($value, TRUE)) { form_error($element, $form_state, t('The URL %url is not valid.', array('%url' => $value))); } } /** * Form element validation handler for #type 'color'. */ function form_validate_color(&$element, &$form_state) { $value = trim($element['#value']); // Default to black if no value is given. // @see http://www.w3.org/TR/html5/number-state.html#color-state if ($value === '') { form_set_value($element, '#000000', $form_state); } else { // Try to parse the value and normalize it. try { form_set_value($element, Color::rgbToHex(Color::hexToRgb($value)), $form_state); } catch (InvalidArgumentException $e) { form_error($element, $form_state, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); } } } /** * Prepares a #type 'color' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_color($element) { $element['#attributes']['type'] = 'color'; element_set_attributes($element, array('id', 'name', 'value')); _form_set_attributes($element, array('form-color')); return $element; } /** * Prepares variables for form templates. * * Default template: form.html.twig. * * @param $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #action, #method, #attributes, #children */ function template_preprocess_form(&$variables) { $element = $variables['element']; if (isset($element['#action'])) { $element['#attributes']['action'] = UrlHelper::stripDangerousProtocols($element['#action']); } element_set_attributes($element, array('method', 'id')); if (empty($element['#attributes']['accept-charset'])) { $element['#attributes']['accept-charset'] = "UTF-8"; } $variables['attributes'] = $element['#attributes']; $variables['children'] = $element['#children']; } /** * Prepares variables for textarea templates. * * Default template: textarea.html.twig. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #title, #value, #description, #rows, #cols, * #placeholder, #required, #attributes, #resizable * */ function template_preprocess_textarea(&$variables) { $element = $variables['element']; element_set_attributes($element, array('id', 'name', 'rows', 'cols', 'placeholder')); _form_set_attributes($element, array('form-textarea')); $variables['wrapper_attributes'] = new Attribute(array( 'class' => array('form-textarea-wrapper'), )); // Add resizable behavior. if (!empty($element['#resizable'])) { $element['#attributes']['class'][] = 'resize-' . $element['#resizable']; } $variables['attributes'] = new Attribute($element['#attributes']); $variables['value'] = String::checkPlain($element['#value']); } /** * Prepares a #type 'password' render element for theme_input(). * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #value, #description, #size, #maxlength, * #placeholder, #required, #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_password($element) { $element['#attributes']['type'] = 'password'; element_set_attributes($element, array('id', 'name', 'size', 'maxlength', 'placeholder')); _form_set_attributes($element, array('form-text')); return $element; } /** * Expands a weight element into a select element. */ function form_process_weight($element) { $element['#is_weight'] = TRUE; // If the number of options is small enough, use a select field. $max_elements = \Drupal::config('system.site')->get('weight_select_max'); if ($element['#delta'] <= $max_elements) { $element['#type'] = 'select'; for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) { $weights[$n] = $n; } $element['#options'] = $weights; $element += element_info('select'); } // Otherwise, use a text field. else { $element['#type'] = 'number'; // Use a field big enough to fit most weights. $element['#size'] = 10; $element += element_info('number'); } return $element; } /** * Prepares a #type 'file' render element for theme_input(). * * For assistance with handling the uploaded file correctly, see the API * provided by file.inc. * * @param array $element * An associative array containing the properties of the element. * Properties used: #title, #name, #size, #description, #required, * #attributes. * * @return array * The $element with prepared variables ready for theme_input(). */ function form_pre_render_file($element) { $element['#attributes']['type'] = 'file'; element_set_attributes($element, array('id', 'name', 'size')); _form_set_attributes($element, array('form-file')); return $element; } /** * Processes a file upload element, make use of #multiple if present. */ function form_process_file($element) { if ($element['#multiple']) { $element['#attributes'] = array('multiple' => 'multiple'); $element['#name'] .= '[]'; } return $element; } /** * Returns HTML for a form element. * Prepares variables for form element templates. * * Default template: form-element.html.twig. * * Each form element is wrapped in a DIV container having the following CSS * classes: * - form-item: Generic for all form elements. * - form-type-#type: The internal element #type. * - form-item-#name: The internal form element #name (usually derived from the * $form structure and set via form_builder()). * - form-disabled: Only set if the form element is #disabled. * * In addition to the element itself, the DIV contains a label for the element * based on the optional #title_display property, and an optional #description. * * The optional #title_display property can have these values: * - before: The label is output before the element. This is the default. * The label includes the #title and the required marker, if #required. * - after: The label is output after the element. For example, this is used * for radio and checkbox #type elements as set in system_element_info(). * If the #title is empty but the field is #required, the label will * contain only the required marker. * - invisible: Labels are critical for screen readers to enable them to * properly navigate through forms but can be visually distracting. This * property hides the label for everyone except screen readers. * - attribute: Set the title attribute on the element to create a tooltip * but output no label element. This is supported only for checkboxes * and radios in form_pre_render_conditional_form_element(). It is used * where a visual label is not needed, such as a table of checkboxes where * the row and column provide the context. The tooltip will include the * title and required marker. * * If the #title property is not set, then the label and any required marker * will not be output, regardless of the #title_display or #required values. * This can be useful in cases such as the password_confirm element, which * creates children elements that have their own labels and required markers, * but the parent element should have neither. Use this carefully because a * field without an associated label can cause accessibility challenges. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #title, #title_display, #description, #id, #required, * #children, #type, #name. * * @ingroup themeable */ function template_preprocess_form_element(&$variables) { $element = &$variables['element']; // This function is invoked as theme wrapper, but the rendered form element // may not necessarily have been processed by form_builder(). $element += array( '#title_display' => 'before', ); // Take over any #wrapper_attributes defined by the element. // @todo Temporary hack for #type 'item'. // @see http://drupal.org/node/1829202 $variables['attributes'] = array(); if (isset($element['#wrapper_attributes'])) { $variables['attributes'] = $element['#wrapper_attributes']; } // Add element #id for #type 'item'. if (isset($element['#markup']) && !empty($element['#id'])) { $variables['attributes']['id'] = $element['#id']; } // Add element's #type and #name as class to aid with JS/CSS selectors. $variables['attributes']['class'][] = 'form-item'; if (!empty($element['#type'])) { $variables['attributes']['class'][] = 'form-type-' . strtr($element['#type'], '_', '-'); } if (!empty($element['#name'])) { $variables['attributes']['class'][] = 'form-item-' . strtr($element['#name'], array(' ' => '-', '_' => '-', '[' => '-', ']' => '')); } // Add a class for disabled elements to facilitate cross-browser styling. if (!empty($element['#attributes']['disabled'])) { $variables['attributes']['class'][] = 'form-disabled'; } // If #title is not set, we don't display any label or required marker. if (!isset($element['#title'])) { $element['#title_display'] = 'none'; } $variables['prefix'] = isset($element['#field_prefix']) ? $element['#field_prefix'] : NULL; $variables['suffix'] = isset($element['#field_suffix']) ? $element['#field_suffix'] : NULL; $variables['description'] = NULL; if (!empty($element['#description'])) { $description_attributes = array('class' => 'description'); if (!empty($element['#id'])) { $description_attributes['id'] = $element['#id'] . '--description'; } $variables['description']['attributes'] = new Attribute($description_attributes); $variables['description']['content'] = $element['#description']; } // Add label_display and label variables to template. $variables['label_display'] = $element['#title_display']; $variables['label'] = array('#theme' => 'form_element_label'); $variables['label'] += array_intersect_key($element, array_flip(array('#id', '#required', '#title', '#title_display'))); $variables['children'] = $element['#children']; } /** * Returns HTML for a marker for required form elements. * * @param $variables * An associative array containing: * - element: An associative array containing the properties of the element. * * @ingroup themeable */ function theme_form_required_marker($variables) { $attributes = array( 'class' => 'form-required', 'aria-hidden' => 'true', ); return '*'; } /** * Returns HTML for a form element label and required marker. * * Form element labels include the #title and a #required marker. The label is * associated with the element itself by the element #id. Labels may appear * before or after elements, depending on theme_form_element() and * #title_display. * * This function will not be called for elements with no labels, depending on * #title_display. For elements that have an empty #title and are not required, * this function will output no label (''). For required elements that have an * empty #title, this will output the required marker alone within the label. * The label will use the #id to associate the marker with the field that is * required. That is especially important for screenreader users to know * which field is required. * * @param $variables * An associative array containing: * - element: An associative array containing the properties of the element. * Properties used: #required, #title, #id, #value, #description. * * @ingroup themeable */ function theme_form_element_label($variables) { $element = $variables['element']; // If title and required marker are both empty, output no label. if ((!isset($element['#title']) || $element['#title'] === '') && empty($element['#required'])) { return ''; } // If the element is required, a required marker is appended to the label. $required = ''; if (!empty($element['#required'])) { $marker = array( '#theme' => 'form_required_marker', '#element' => $element, ); $required = drupal_render($marker); } $title = filter_xss_admin($element['#title']); $attributes = array(); // Style the label as class option to display inline with the element. if ($element['#title_display'] == 'after') { $attributes['class'] = 'option'; } // Show label only to screen readers to avoid disruption in visual flows. elseif ($element['#title_display'] == 'invisible') { $attributes['class'] = 'visually-hidden'; } // A #for property of a dedicated #type 'label' element as precedence. if (!empty($element['#for'])) { $attributes['for'] = $element['#for']; // A custom #id allows the referenced form input element to refer back to // the label element; e.g., in the 'aria-labelledby' attribute. if (!empty($element['#id'])) { $attributes['id'] = $element['#id']; } } // Otherwise, point to the #id of the form input element. elseif (!empty($element['#id'])) { $attributes['for'] = $element['#id']; } return '' . t('!title!required', array('!title' => $title, '!required' => $required)) . ''; } /** * Sets a form element's class attribute. * * Adds 'required' and 'error' classes as needed. * * @param $element * The form element. * @param $name * Array of new class names to be added. */ function _form_set_attributes(&$element, $class = array()) { if (!empty($class)) { if (!isset($element['#attributes']['class'])) { $element['#attributes']['class'] = array(); } $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class); } // This function is invoked from form element theme functions, but the // rendered form element may not necessarily have been processed by // form_builder(). if (!empty($element['#required'])) { $element['#attributes']['class'][] = 'required'; $element['#attributes']['required'] = 'required'; $element['#attributes']['aria-required'] = 'true'; } if (isset($element['#parents']) && isset($element['#errors']) && !empty($element['#validated'])) { $element['#attributes']['class'][] = 'error'; $element['#attributes']['aria-invalid'] = 'true'; } } /** * @} End of "defgroup form_api". */ /** * @defgroup batch Batch operations * @{ * Creates and processes batch operations. * * Functions allowing forms processing to be spread out over several page * requests, thus ensuring that the processing does not get interrupted * because of a PHP timeout, while allowing the user to receive feedback * on the progress of the ongoing operations. * * The API is primarily designed to integrate nicely with the Form API * workflow, but can also be used by non-Form API scripts (like update.php) * or even simple page callbacks (which should probably be used sparingly). * * Example: * @code * $batch = array( * 'title' => t('Exporting'), * 'operations' => array( * array('my_function_1', array($account->id(), 'story')), * array('my_function_2', array()), * ), * 'finished' => 'my_finished_callback', * 'file' => 'path_to_file_containing_myfunctions', * ); * batch_set($batch); * // Only needed if not inside a form _submit handler. * // Setting redirect in batch_process. * batch_process('node/1'); * @endcode * * Note: if the batch 'title', 'init_message', 'progress_message', or * 'error_message' could contain any user input, it is the responsibility of * the code calling batch_set() to sanitize them first with a function like * \Drupal\Component\Utility\String::checkPlain() or filter_xss(). Furthermore, * if the batch operation returns any user input in the 'results' or 'message' * keys of $context, it must also sanitize them first. * * Sample callback_batch_operation(): * @code * // Simple and artificial: load a node of a given type for a given user * function my_function_1($uid, $type, &$context) { * // The $context array gathers batch context information about the execution (read), * // as well as 'return values' for the current operation (write) * // The following keys are provided : * // 'results' (read / write): The array of results gathered so far by * // the batch processing, for the current operation to append its own. * // 'message' (write): A text message displayed in the progress page. * // The following keys allow for multi-step operations : * // 'sandbox' (read / write): An array that can be freely used to * // store persistent data between iterations. It is recommended to * // use this instead of $_SESSION, which is unsafe if the user * // continues browsing in a separate window while the batch is processing. * // 'finished' (write): A float number between 0 and 1 informing * // the processing engine of the completion level for the operation. * // 1 (or no value explicitly set) means the operation is finished * // and the batch processing can continue to the next operation. * * $nodes = entity_load_multiple_by_properties('node', array('uid' => $uid, 'type' => $type)); * $node = reset($nodes); * $context['results'][] = $node->id() . ' : ' . String::checkPlain($node->label()); * $context['message'] = String::checkPlain($node->label()); * } * * // A more advanced example is a multi-step operation that loads all rows, * // five by five. * function my_function_2(&$context) { * if (empty($context['sandbox'])) { * $context['sandbox']['progress'] = 0; * $context['sandbox']['current_id'] = 0; * $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT id) FROM {example}')->fetchField(); * } * $limit = 5; * $result = db_select('example') * ->fields('example', array('id')) * ->condition('id', $context['sandbox']['current_id'], '>') * ->orderBy('id') * ->range(0, $limit) * ->execute(); * foreach ($result as $row) { * $context['results'][] = $row->id . ' : ' . String::checkPlain($row->title); * $context['sandbox']['progress']++; * $context['sandbox']['current_id'] = $row->id; * $context['message'] = String::checkPlain($row->title); * } * if ($context['sandbox']['progress'] != $context['sandbox']['max']) { * $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; * } * } * @endcode * * Sample callback_batch_finished(): * @code * function batch_test_finished($success, $results, $operations) { * // The 'success' parameter means no fatal PHP errors were detected. All * // other error management should be handled using 'results'. * if ($success) { * $message = format_plural(count($results), 'One post processed.', '@count posts processed.'); * } * else { * $message = t('Finished with an error.'); * } * drupal_set_message($message); * // Providing data for the redirected page is done through $_SESSION. * foreach ($results as $result) { * $items[] = t('Loaded node %title.', array('%title' => $result)); * } * $_SESSION['my_batch_results'] = $items; * } * @endcode */ /** * Adds a new batch. * * Batch operations are added as new batch sets. Batch sets are used to spread * processing (primarily, but not exclusively, forms processing) over several * page requests. This helps to ensure that the processing is not interrupted * due to PHP timeouts, while users are still able to receive feedback on the * progress of the ongoing operations. Combining related operations into * distinct batch sets provides clean code independence for each batch set, * ensuring that two or more batches, submitted independently, can be processed * without mutual interference. Each batch set may specify its own set of * operations and results, produce its own UI messages, and trigger its own * 'finished' callback. Batch sets are processed sequentially, with the progress * bar starting afresh for each new set. * * @param $batch_definition * An associative array defining the batch, with the following elements (all * are optional except as noted): * - operations: (required) Array of operations to be performed, where each * item is an array consisting of the name of an implementation of * callback_batch_operation() and an array of parameter. * Example: * @code * array( * array('callback_batch_operation_1', array($arg1)), * array('callback_batch_operation_2', array($arg2_1, $arg2_2)), * ) * @endcode * - title: A safe, translated string to use as the title for the progress * page. Defaults to t('Processing'). * - init_message: Message displayed while the processing is initialized. * Defaults to t('Initializing.'). * - progress_message: Message displayed while processing the batch. Available * placeholders are @current, @remaining, @total, @percentage, @estimate and * @elapsed. Defaults to t('Completed @current of @total.'). * - error_message: Message displayed if an error occurred while processing * the batch. Defaults to t('An error has occurred.'). * - finished: Name of an implementation of callback_batch_finished(). This is * executed after the batch has completed. This should be used to perform * any result massaging that may be needed, and possibly save data in * $_SESSION for display after final page redirection. * - file: Path to the file containing the definitions of the 'operations' and * 'finished' functions, for instance if they don't reside in the main * .module file. The path should be relative to base_path(), and thus should * be built using drupal_get_path(). * - css: Array of paths to CSS files to be used on the progress page. * - url_options: options passed to url() when constructing redirect URLs for * the batch. */ function batch_set($batch_definition) { if ($batch_definition) { $batch =& batch_get(); // Initialize the batch if needed. if (empty($batch)) { $batch = array( 'sets' => array(), 'has_form_submits' => FALSE, ); } // Base and default properties for the batch set. $init = array( 'sandbox' => array(), 'results' => array(), 'success' => FALSE, 'start' => 0, 'elapsed' => 0, ); $defaults = array( 'title' => t('Processing'), 'init_message' => t('Initializing.'), 'progress_message' => t('Completed @current of @total.'), 'error_message' => t('An error has occurred.'), 'css' => array(), ); $batch_set = $init + $batch_definition + $defaults; // Tweak init_message to avoid the bottom of the page flickering down after // init phase. $batch_set['init_message'] .= '
 '; // The non-concurrent workflow of batch execution allows us to save // numberOfItems() queries by handling our own counter. $batch_set['total'] = count($batch_set['operations']); $batch_set['count'] = $batch_set['total']; // Add the set to the batch. if (empty($batch['id'])) { // The batch is not running yet. Simply add the new set. $batch['sets'][] = $batch_set; } else { // The set is being added while the batch is running. Insert the new set // right after the current one to ensure execution order, and store its // operations in a queue. $index = $batch['current_set'] + 1; $slice1 = array_slice($batch['sets'], 0, $index); $slice2 = array_slice($batch['sets'], $index); $batch['sets'] = array_merge($slice1, array($batch_set), $slice2); _batch_populate_queue($batch, $index); } } } /** * Processes the batch. * * This function is generally not needed in form submit handlers; * Form API takes care of batches that were set during form submission. * * @param $redirect * (optional) Path to redirect to when the batch has finished processing. * @param $url * (optional - should only be used for separate scripts like update.php) * URL of the batch processing page. * @param $redirect_callback * (optional) Specify a function to be called to redirect to the progressive * processing page. * * @return \Symfony\Component\HttpFoundation\RedirectResponse|null * A redirect response if the batch is progressive. No return value otherwise. */ function batch_process($redirect = NULL, $url = 'batch', $redirect_callback = NULL) { $batch =& batch_get(); drupal_theme_initialize(); if (isset($batch)) { // Add process information $process_info = array( 'current_set' => 0, 'progressive' => TRUE, 'url' => $url, 'url_options' => array(), 'source_url' => current_path(), 'redirect' => $redirect, 'theme' => $GLOBALS['theme_key'], 'redirect_callback' => $redirect_callback, ); $batch += $process_info; // The batch is now completely built. Allow other modules to make changes // to the batch so that it is easier to reuse batch processes in other // environments. \Drupal::moduleHandler()->alter('batch', $batch); // Assign an arbitrary id: don't rely on a serial column in the 'batch' // table, since non-progressive batches skip database storage completely. $batch['id'] = db_next_id(); // Move operations to a job queue. Non-progressive batches will use a // memory-based queue. foreach ($batch['sets'] as $key => $batch_set) { _batch_populate_queue($batch, $key); } // Initiate processing. if ($batch['progressive']) { // Now that we have a batch id, we can generate the redirection link in // the generic error message. $batch['error_message'] = t('Please continue to the error page', array('@error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'finished'))))); // Clear the way for the redirection to the batch processing page, by // saving and unsetting the 'destination', if there is any. $request = \Drupal::request(); if ($request->query->has('destination')) { $batch['destination'] = $request->query->get('destination'); $request->query->remove('destination'); } // Store the batch. \Drupal::service('batch.storage')->create($batch); // Set the batch number in the session to guarantee that it will stay alive. $_SESSION['batches'][$batch['id']] = TRUE; // Redirect for processing. $options = array('query' => array('op' => 'start', 'id' => $batch['id'])); if (($function = $batch['redirect_callback']) && function_exists($function)) { $function($batch['url'], $options); } else { $options['absolute'] = TRUE; return new RedirectResponse(url($batch['url'], $options)); } } else { // Non-progressive execution: bypass the whole progressbar workflow // and execute the batch in one pass. require_once __DIR__ . '/batch.inc'; _batch_process(); } } } /** * Retrieves the current batch. */ function &batch_get() { // Not drupal_static(), because Batch API operates at a lower level than most // use-cases for resetting static variables, and we specifically do not want a // global drupal_static_reset() resetting the batch information. Functions // that are part of the Batch API and need to reset the batch information may // call batch_get() and manipulate the result by reference. Functions that are // not part of the Batch API can also do this, but shouldn't. static $batch = array(); return $batch; } /** * Populates a job queue with the operations of a batch set. * * Depending on whether the batch is progressive or not, the * Drupal\Core\Queue\Batch or Drupal\Core\Queue\BatchMemory handler classes will * be used. * * @param $batch * The batch array. * @param $set_id * The id of the set to process. * * @return * The name and class of the queue are added by reference to the batch set. */ function _batch_populate_queue(&$batch, $set_id) { $batch_set = &$batch['sets'][$set_id]; if (isset($batch_set['operations'])) { $batch_set += array( 'queue' => array( 'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id, 'class' => $batch['progressive'] ? 'Drupal\Core\Queue\Batch' : 'Drupal\Core\Queue\BatchMemory', ), ); $queue = _batch_queue($batch_set); $queue->createQueue(); foreach ($batch_set['operations'] as $operation) { $queue->createItem($operation); } unset($batch_set['operations']); } } /** * Returns a queue object for a batch set. * * @param $batch_set * The batch set. * * @return * The queue object. */ function _batch_queue($batch_set) { static $queues; if (!isset($queues)) { $queues = array(); } if (isset($batch_set['queue'])) { $name = $batch_set['queue']['name']; $class = $batch_set['queue']['class']; if (!isset($queues[$class][$name])) { $queues[$class][$name] = new $class($name, Database::getConnection()); } return $queues[$class][$name]; } } /** * @} End of "defgroup batch". */