uc_ajax_attach.inc 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <?php
  2. /**
  3. * @file
  4. * Contains logic to aid in attaching multiple ajax behaviors to form
  5. * elements on the checkout and order-edit forms.
  6. *
  7. * Both the checkout and the order edit forms are made up of multiple panes,
  8. * many supplied by contrib modules. Any pane may wish to update its own
  9. * display or that of another pane based on user input from input elements
  10. * anywhere on the form. The mechanism here described enables modules
  11. * to add ajax behaviors to the form in an orderly and efficient manner.
  12. *
  13. * Generally, an implementing pane should not add #ajax keys to existing form
  14. * elements directly. Rather, it should attach ajax behavior by adding
  15. * to the $form_state['uc_ajax'] array.
  16. *
  17. * $form_state['uc_ajax'] is an associative array keyed by the name of the
  18. * implementing module. Each implementing module should provide an array
  19. * of ajax callbacks, keyed by the name of the triggering element as it would
  20. * be specified when invoking form_set_error(). The entry for each element
  21. * may be either the name of a single ajax callback to be attached to that
  22. * element, or an array of ajax callbacks, optionally keyed by wrapper.
  23. * For example:
  24. *
  25. * @code
  26. * $form_state['uc_ajax']['mymodule']['panes][quotes][quote_button'] = array(
  27. * 'quotes-pane' => 'uc_ajax_replace_checkout_pane',
  28. * );
  29. * @endcode
  30. *
  31. * This will cause the contents of 'quotes-pane' to be replaced by the return
  32. * value of uc_ajax_replace_checkout_pane(). Note that if more than one module
  33. * assign a callback to the same wrapper key, the heavier module or pane will
  34. * take precedence.
  35. *
  36. * Implementors need not provide a wrapper key for each callback, in which case
  37. * the callback must return an array of ajax commands rather than a renderable
  38. * form element. For example:
  39. *
  40. * @code
  41. * $form_state['uc_ajax']['mymodule']['panes][quotes][quote_button'] = array('my_ajax_callback');
  42. * ...
  43. * function my_ajax_callback($form, $form_state) {
  44. * $commands[] = ajax_command_invoke('#my-input-element', 'val', 0);
  45. * return array('#type' => 'ajax', '#commands' => $commands);
  46. * }
  47. * @endcode
  48. *
  49. * However, using a wrapper key where appropriate will reduce redundant
  50. * replacements of the same element.
  51. *
  52. * NOTE: 'uc_ajax_replace_checkout_pane' is a convenience callback which will
  53. * replace the contents of an entire checkout pane. It is generally preferable
  54. * to use this when updating data on the checkout form, as this will
  55. * further reduce the likelihood of redundant replacements. You should use
  56. * your own callback only when behaviours other than replacement are
  57. * desired, or when replacing data that lie outside a checkout pane. Note
  58. * also that you may combine both formulations by mixing numeric and string keys.
  59. * For example:
  60. *
  61. * @code
  62. * $form_state['uc_ajax']['mymodule']['panes][quotes][quote_button'] = array(
  63. * 'my_ajax_callback',
  64. * 'quotes-pane' => 'uc_ajax_replace_checkout_pane',
  65. * );
  66. * @endcode
  67. */
  68. /**
  69. * Form process callback to allow multiple Ajax callbacks on form elements.
  70. */
  71. function uc_ajax_process_form($form, &$form_state) {
  72. // When processing the top level form, add any variable-defined pane wrappers.
  73. if (isset($form['#form_id'])) {
  74. switch ($form['#form_id']) {
  75. case 'uc_cart_checkout_form':
  76. $config = variable_get('uc_ajax_checkout', _uc_ajax_defaults('checkout'));
  77. foreach ($config as $key => $panes) {
  78. foreach (array_keys($panes) as $pane) {
  79. $config[$key][$pane] = 'uc_ajax_replace_checkout_pane';
  80. }
  81. }
  82. $form_state['uc_ajax']['uc_ajax'] = $config;
  83. break;
  84. }
  85. }
  86. if (!isset($form_state['uc_ajax'])) {
  87. return $form;
  88. }
  89. // We have to operate on the children rather than on the element itself, as
  90. // #process functions are called *after* form_handle_input_elements(),
  91. // which is where the triggering element is determined. If we haven't added
  92. // an '#ajax' key by that time, Drupal won't be able to determine which
  93. // callback to invoke.
  94. foreach (element_children($form) as $child) {
  95. $element =& $form[$child];
  96. // Add this process function recursively to the children.
  97. if (empty($element['#process']) && !empty($element['#type'])) {
  98. // We want to be sure the default process functions for the element type are called.
  99. $info = element_info($element['#type']);
  100. if (!empty($info['#process'])) {
  101. $element['#process'] = $info['#process'];
  102. }
  103. }
  104. $element['#process'][] = 'uc_ajax_process_form';
  105. // Multiplex any Ajax calls for this element.
  106. $parents = $form['#array_parents'];
  107. array_push($parents, $child);
  108. $key = implode('][', $parents);
  109. $callbacks = array();
  110. foreach ($form_state['uc_ajax'] as $module => $fields) {
  111. if (!empty($fields[$key])) {
  112. if (is_array($fields[$key])) {
  113. $callbacks = array_merge($callbacks, $fields[$key]);
  114. }
  115. else {
  116. $callbacks[] = $fields[$key];
  117. }
  118. }
  119. }
  120. if (!empty($callbacks)) {
  121. if (empty($element['#ajax'])) {
  122. $element['#ajax'] = array();
  123. }
  124. elseif (!empty($element['#ajax']['callback'])) {
  125. if (!empty($element['#ajax']['wrapper'])) {
  126. $callbacks[$element['#ajax']['wrapper']] = $element['#ajax']['callback'];
  127. }
  128. else {
  129. array_unshift($callbacks, $element['#ajax']['callback']);
  130. }
  131. }
  132. $element['#ajax'] = array_merge($element['#ajax'], array(
  133. 'callback' => 'uc_ajax_multiplex',
  134. 'list' => $callbacks,
  135. ));
  136. }
  137. }
  138. return $form;
  139. }
  140. /**
  141. * Ajax callback multiplexer.
  142. *
  143. * Processes a set of Ajax commands attached to the triggering element.
  144. */
  145. function uc_ajax_multiplex($form, $form_state) {
  146. $element = $form_state['triggering_element'];
  147. if (!empty($element['#ajax']['list'])) {
  148. $commands = array();
  149. foreach ($element['#ajax']['list'] as $wrapper => $callback) {
  150. if (!empty($callback) && function_exists($callback) && $result = $callback($form, $form_state, $wrapper)) {
  151. if (is_array($result) && !empty($result['#type']) && $result['#type'] == 'ajax') {
  152. // If the callback returned an array of commands, simply add these to the list.
  153. $commands = array_merge($commands, $result['#commands']);
  154. }
  155. elseif (is_string($wrapper)) {
  156. // Otherwise, assume the callback returned a string or render-array, and insert it into the wrapper.
  157. $html = is_string($result) ? $result : drupal_render($result);
  158. $commands[] = ajax_command_replace('#' . $wrapper, trim($html));
  159. $commands[] = ajax_command_prepend('#' . $wrapper, theme('status_messages'));
  160. }
  161. }
  162. }
  163. }
  164. if (!empty($commands)) {
  165. return array('#type' => 'ajax', '#commands' => $commands);
  166. }
  167. }
  168. /**
  169. * Ajax callback to replace a whole checkout pane.
  170. *
  171. * @param $form
  172. * The checkout form.
  173. * @param $form_state
  174. * The current form state.
  175. * @param $wrapper
  176. * Special third parameter passed for uc_ajax callbacks containing the ajax
  177. * wrapper for this callback. Here used to determine which pane to replace.
  178. *
  179. * @return
  180. * The form element representing the pane, suitable for ajax rendering. If
  181. * the pane does not exist, or if the wrapper does not refer to a checkout
  182. * pane, returns nothing.
  183. */
  184. function uc_ajax_replace_checkout_pane($form, $form_state, $wrapper = NULL) {
  185. if (empty($wrapper) && !empty($form_state['triggering_element']['#ajax']['wrapper'])) {
  186. // If $wrapper is absent, then we were not invoked by uc_ajax_multiplex,
  187. // so try to use the wrapper of the triggering element's #ajax array.
  188. $wrapper = $form_state['triggering_element']['#ajax']['wrapper'];
  189. }
  190. if (!empty($wrapper)) {
  191. list($pane, $verify) = explode('-', $wrapper);
  192. if ($verify === 'pane' && !empty($form['panes'][$pane])) {
  193. return $form['panes'][$pane];
  194. }
  195. }
  196. }
  197. /**
  198. * Retrieve the default ajax behaviors for a target form.
  199. *
  200. * @param $target_form
  201. * The form whose default behaviors are to be retrieved.
  202. *
  203. * @return
  204. * The array of default behaviors for the form.
  205. */
  206. function _uc_ajax_defaults($target_form) {
  207. switch ($target_form) {
  208. case 'checkout':
  209. $quotes_defaults = drupal_map_assoc(array('payment-pane', 'quotes-pane'));
  210. return array(
  211. 'panes][delivery][address][delivery_country' => $quotes_defaults,
  212. 'panes][delivery][address][delivery_postal_code' => $quotes_defaults,
  213. 'panes][delivery][select_address' => $quotes_defaults,
  214. 'panes][billing][address][billing_country' => array('payment-pane' => 'payment-pane'),
  215. );
  216. default:
  217. return array();
  218. }
  219. }