FormHelper.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <?php
  2. namespace Drupal\Core\Form;
  3. use Drupal\Component\Serialization\Json;
  4. use Drupal\Core\Render\Element;
  5. /**
  6. * Provides helpers to operate on forms.
  7. *
  8. * @ingroup form_api
  9. */
  10. class FormHelper {
  11. /**
  12. * Rewrites #states selectors in a render element.
  13. *
  14. * When a structure of elements is being altered, their HTML selectors may
  15. * change. In such cases calling this method will check if there are any
  16. * states in element and its children, and rewrite selectors in those states.
  17. *
  18. * @param array $elements
  19. * A render array element having a #states property.
  20. * @param string $search
  21. * A partial or entire jQuery selector string to replace in #states.
  22. * @param string $replace
  23. * The string to replace all instances of $search with.
  24. *
  25. * @see self::processStates()
  26. */
  27. public static function rewriteStatesSelector(array &$elements, $search, $replace) {
  28. if (!empty($elements['#states'])) {
  29. foreach ($elements['#states'] as $state => $ids) {
  30. static::processStatesArray($elements['#states'][$state], $search, $replace);
  31. }
  32. }
  33. foreach (Element::children($elements) as $key) {
  34. static::rewriteStatesSelector($elements[$key], $search, $replace);
  35. }
  36. }
  37. /**
  38. * Helps recursively rewrite #states selectors.
  39. *
  40. * Not to be confused with self::processStates(), which just prepares states
  41. * for rendering.
  42. *
  43. * @param array $conditions
  44. * States conditions array.
  45. * @param string $search
  46. * A partial or entire jQuery selector string to replace in #states.
  47. * @param string $replace
  48. * The string to replace all instances of $search with.
  49. *
  50. * @see self::rewriteStatesSelector()
  51. */
  52. protected static function processStatesArray(array &$conditions, $search, $replace) {
  53. // Retrieve the keys to make it easy to rename a key without changing the
  54. // order of an array.
  55. $keys = array_keys($conditions);
  56. $update_keys = FALSE;
  57. foreach ($conditions as $id => $values) {
  58. if (strpos($id, $search) !== FALSE) {
  59. $update_keys = TRUE;
  60. $new_id = str_replace($search, $replace, $id);
  61. // Replace the key and keep the array in the same order.
  62. $index = array_search($id, $keys, TRUE);
  63. $keys[$index] = $new_id;
  64. }
  65. elseif (is_array($values)) {
  66. static::processStatesArray($conditions[$id], $search, $replace);
  67. }
  68. }
  69. // Updates the states conditions keys if necessary.
  70. if ($update_keys) {
  71. $conditions = array_combine($keys, array_values($conditions));
  72. }
  73. }
  74. /**
  75. * Adds JavaScript to change the state of an element based on another element.
  76. *
  77. * A "state" means a certain property of a DOM element, such as "visible" or
  78. * "checked", which depends on a state or value of another element on the
  79. * page. In general, states are HTML attributes and DOM element properties,
  80. * which are applied initially, when page is loaded, depending on elements'
  81. * default values, and then may change due to user interaction.
  82. *
  83. * Since states are driven by JavaScript only, it is important to understand
  84. * that all states are applied on presentation only, none of the states force
  85. * any server-side logic, and that they will not be applied for site visitors
  86. * without JavaScript support. All modules implementing states have to make
  87. * sure that the intended logic also works without JavaScript being enabled.
  88. *
  89. * #states is an associative array in the form of:
  90. * @code
  91. * [
  92. * STATE1 => CONDITIONS_ARRAY1,
  93. * STATE2 => CONDITIONS_ARRAY2,
  94. * ...
  95. * ]
  96. * @endcode
  97. * Each key is the name of a state to apply to the element, such as 'visible'.
  98. * Each value is a list of conditions that denote when the state should be
  99. * applied.
  100. *
  101. * Multiple different states may be specified to act on complex conditions:
  102. * @code
  103. * [
  104. * 'visible' => CONDITIONS,
  105. * 'checked' => OTHER_CONDITIONS,
  106. * ]
  107. * @endcode
  108. *
  109. * Every condition is a key/value pair, whose key is a jQuery selector that
  110. * denotes another element on the page, and whose value is an array of
  111. * conditions, which must bet met on that element:
  112. * @code
  113. * [
  114. * 'visible' => [
  115. * JQUERY_SELECTOR => REMOTE_CONDITIONS,
  116. * JQUERY_SELECTOR => REMOTE_CONDITIONS,
  117. * ...
  118. * ],
  119. * ]
  120. * @endcode
  121. * All conditions must be met for the state to be applied.
  122. *
  123. * Each remote condition is a key/value pair specifying conditions on the
  124. * other element that need to be met to apply the state to the element:
  125. * @code
  126. * [
  127. * 'visible' => [
  128. * ':input[name="remote_checkbox"]' => ['checked' => TRUE],
  129. * ],
  130. * ]
  131. * @endcode
  132. *
  133. * For example, to show a textfield only when a checkbox is checked:
  134. * @code
  135. * $form['toggle_me'] = [
  136. * '#type' => 'checkbox',
  137. * '#title' => t('Tick this box to type'),
  138. * ];
  139. * $form['settings'] = [
  140. * '#type' => 'textfield',
  141. * '#states' => [
  142. * // Only show this field when the 'toggle_me' checkbox is enabled.
  143. * 'visible' => [
  144. * ':input[name="toggle_me"]' => ['checked' => TRUE],
  145. * ],
  146. * ],
  147. * ];
  148. * @endcode
  149. *
  150. * The following states may be applied to an element:
  151. * - enabled
  152. * - disabled
  153. * - required
  154. * - optional
  155. * - visible
  156. * - invisible
  157. * - checked
  158. * - unchecked
  159. * - expanded
  160. * - collapsed
  161. *
  162. * The following states may be used in remote conditions:
  163. * - empty
  164. * - filled
  165. * - checked
  166. * - unchecked
  167. * - expanded
  168. * - collapsed
  169. * - value
  170. *
  171. * The following states exist for both elements and remote conditions, but are
  172. * not fully implemented and may not change anything on the element:
  173. * - relevant
  174. * - irrelevant
  175. * - valid
  176. * - invalid
  177. * - touched
  178. * - untouched
  179. * - readwrite
  180. * - readonly
  181. *
  182. * When referencing select lists and radio buttons in remote conditions, a
  183. * 'value' condition must be used:
  184. * @code
  185. * '#states' => [
  186. * // Show the settings if 'bar' has been selected for 'foo'.
  187. * 'visible' => [
  188. * ':input[name="foo"]' => ['value' => 'bar'],
  189. * ],
  190. * ],
  191. * @endcode
  192. *
  193. * @param array $elements
  194. * A render array element having a #states property as described above.
  195. */
  196. public static function processStates(array &$elements) {
  197. $elements['#attached']['library'][] = 'core/drupal.states';
  198. // Elements of '#type' => 'item' are not actual form input elements, but we
  199. // still want to be able to show/hide them. Since there's no actual HTML
  200. // input element available, setting #attributes does not make sense, but a
  201. // wrapper is available, so setting #wrapper_attributes makes it work.
  202. $key = ($elements['#type'] == 'item') ? '#wrapper_attributes' : '#attributes';
  203. $elements[$key]['data-drupal-states'] = Json::encode($elements['#states']);
  204. }
  205. }