diff --git a/payment/uc_payment/uc_payment.module b/payment/uc_payment/uc_payment.module index 2527560..6e210f6 100644 --- a/payment/uc_payment/uc_payment.module +++ b/payment/uc_payment/uc_payment.module @@ -248,7 +248,9 @@ function uc_payment_uc_order_state() { * The formatted HTML of the order total preview if $return is set to TRUE. */ function uc_payment_get_totals($form, $form_state) { - return $form['panes']['payment']['line_items']; + $commands[] = ajax_command_replace('#line-items-div', trim(drupal_render($form['panes']['payment']['line_items']))); + + return array('#type' => 'ajax', '#commands' => $commands); } /** diff --git a/payment/uc_payment/uc_payment_checkout_pane.inc b/payment/uc_payment/uc_payment_checkout_pane.inc index 35a280d..4da6a32 100644 --- a/payment/uc_payment/uc_payment_checkout_pane.inc +++ b/payment/uc_payment/uc_payment_checkout_pane.inc @@ -69,6 +69,8 @@ function uc_checkout_pane_payment($op, &$order, $form = NULL, &$form_state = NUL 'type' => 'throbber', ), ), + '#prefix' => '
', + '#suffix' => '
', ); $contents['details'] = array( diff --git a/payment/uc_payment/uc_payment_order_pane.inc b/payment/uc_payment/uc_payment_order_pane.inc index 7681c11..bdfa60b 100644 --- a/payment/uc_payment/uc_payment_order_pane.inc +++ b/payment/uc_payment/uc_payment_order_pane.inc @@ -122,7 +122,7 @@ function uc_order_pane_payment($op, $order, &$form = NULL, &$form_state = NULL) * AJAX callback to render the payment method pane. */ function uc_payment_order_pane_ajax_callback($form, &$form_state) { - $commands[] = ajax_command_replace('#payment-details', drupal_render($form['payment']['payment_details'])); + $commands[] = ajax_command_replace('#payment-details', trim(drupal_render($form['payment']['payment_details']))); $commands[] = ajax_command_prepend('#payment-details', theme('status_messages')); return array('#type' => 'ajax', '#commands' => $commands); } diff --git a/shipping/uc_quote/uc_quote.module b/shipping/uc_quote/uc_quote.module index 5a5013e..302f7da 100644 --- a/shipping/uc_quote/uc_quote.module +++ b/shipping/uc_quote/uc_quote.module @@ -241,75 +241,6 @@ function uc_quote_form_alter(&$form, &$form_state, $form_id) { } /** - * Implements hook_form_FORM_ID_alter() for uc_cart_checkout_form(). - * - * Adds Ajax shipping quote functionality to the checkout form. - */ -function uc_quote_form_uc_cart_checkout_form_alter(&$form, &$form_state) { - if (isset($form['panes']['delivery']['address'])) { - $form['panes']['delivery']['address']['#process'] = array('uc_store_process_address_field', 'uc_quote_process_checkout_address'); - } - if (isset($form['panes']['delivery']['select_address'])) { - $form['panes']['delivery']['select_address']['#ajax']['callback'] = 'uc_quote_select_address'; - } - // @todo: Figure out what needs to be done when copy-address box is checked. - - if (isset($form['panes']['payment']['line_items'])) { - $form['panes']['quotes']['quotes']['quote_option']['#ajax'] = array( - 'callback' => 'uc_payment_get_totals', - 'wrapper' => 'line-items-div', - ); - } -} - -/** - * Ajax callback: Updates quotes when a saved address is selected. - */ -function uc_quote_select_address($form, $form_state) { - $return = uc_quote_checkout_returned_rates($form, $form_state); - $address = uc_checkout_pane_address_render($form, $form_state); - - $return['#commands'][] = ajax_command_replace('#' . $form_state['triggering_element']['#ajax']['wrapper'], drupal_render($address)); - - return $return; -} - -/** - * Ajax callback: Updates shipping quotes when the delivery country changes. - */ -function uc_quote_country_change_wrapper(&$form, &$form_state) { - $return = uc_quote_checkout_returned_rates($form, $form_state); - $return['#commands'][] = ajax_command_replace('#uc-store-address-delivery-zone-wrapper', drupal_render(uc_store_update_address_field_zones($form, $form_state))); - return $return; -} - -/** - * Process callback: Adds Ajax functionality to delivery address fields. - */ -function uc_quote_process_checkout_address($element, $form_state) { - $ajax = array( - 'callback' => 'uc_quote_checkout_returned_rates', - 'effect' => 'slide', - 'progress' => array( - 'type' => 'throbber', - 'message' => t('Receiving shipping quotes...'), - ), - ); - - if (isset($element['delivery_postal_code'])) { - $element['delivery_postal_code']['#ajax'] = $ajax; - } - // The following replaces "uc_store_process_address_field" from uc_store - // with a wrapper that will update the available quotes when the country - // is changed. - if (isset($element['delivery_country'])) { - $element['delivery_country']['#ajax']['callback'] = 'uc_quote_country_change_wrapper'; - } - - return $element; -} - -/** * Implements hook_uc_cart_pane(). */ function uc_quote_uc_cart_pane($items) { @@ -626,7 +557,9 @@ function uc_checkout_pane_quotes($op, &$order, $form = NULL, &$form_state = NULL '#submit' => array('uc_quote_checkout_pane_quotes_submit'), '#weight' => 0, '#ajax' => array( - 'callback' => 'uc_quote_checkout_returned_rates', + // The callbacks are set below in $form_state['uc_ajax_attach'] + //'callback' => 'uc_ajax_replace_checkout_pane', + //'wrapper' => 'quotes-pane', 'effect' => 'slide', 'progress' => array( 'type' => 'bar', @@ -645,6 +578,13 @@ function uc_checkout_pane_quotes($op, &$order, $form = NULL, &$form_state = NULL $contents['quotes'] += $order->quote_form; + $ajax_attach = array( + 'payment-pane' => 'uc_ajax_replace_checkout_pane', + 'quotes-pane' => 'uc_ajax_replace_checkout_pane' + ); + $form_state['uc_ajax']['uc_quote']['panes][quotes][quote_button'] = $ajax_attach; + $form_state['uc_ajax']['uc_quote']['panes][quotes][quotes][quote_option'] = array('payment-pane' => 'uc_ajax_replace_checkout_pane'); + return array('description' => $description, 'contents' => $contents); case 'prepare': @@ -750,7 +690,8 @@ function uc_order_pane_quotes($op, $order, &$form = NULL, &$form_state = NULL) { '#value' => t('Get shipping quotes'), '#submit' => array('uc_quote_order_pane_quotes_submit'), '#ajax' => array( - 'callback' => 'uc_quote_order_returned_rates', + 'callback' => 'uc_quote_replace_order_quotes', + 'wrapper' => 'quote', 'effect' => 'slide', 'progress' => array( 'type' => 'bar', @@ -774,6 +715,8 @@ function uc_order_pane_quotes($op, $order, &$form = NULL, &$form_state = NULL) { ); } + $form_state['uc_ajax']['uc_quote']['ship_to][delivery_country'] = array('quote' => 'uc_quote_replace_order_quotes'); + return $form; case 'edit-theme': @@ -914,8 +857,8 @@ function uc_quote_build_quote_form($order, $show_errors = TRUE) { * Ajax callback: Shows estimated shipping quotes on the cart page. */ function uc_quote_cart_returned_rates($form, $form_state) { - $commands[] = ajax_command_replace('#quote', drupal_render($form['quote'])); - $commands[] = ajax_command_prepend('#quote', theme('status_messages')); + $commands[] = ajax_command_replace('#quote', trim(drupal_render($form['quote']))); + $commands[] = ajax_command_prepend('#quote', trim(theme('status_messages'))); return array('#type' => 'ajax', '#commands' => $commands); } @@ -943,29 +886,10 @@ function _uc_quote_extract_default_option($quote_form) { } /** - * AJAX callback for calculated shipping rates. + * Ajax callback to update the quotes on the order edit form. */ -function uc_quote_checkout_returned_rates($form, $form_state) { - $commands[] = ajax_command_replace('#quote', drupal_render($form['panes']['quotes']['quotes'])); - $commands[] = ajax_command_prepend('#quote', theme('status_messages')); - - // Show default shipping rate as a line item. - if (isset($form['panes']['payment']['line_items'])) { - $commands[] = ajax_command_replace('#line-items-div', drupal_render($form['panes']['payment']['line_items'])); - $commands[] = ajax_command_prepend('#line-items-div', theme('status_messages')); - } - - return array('#type' => 'ajax', '#commands' => $commands); -} - -/** - * AJAX callback for calculated shipping rates. - */ -function uc_quote_order_returned_rates($form, $form_state) { - $commands[] = ajax_command_replace('#quote', drupal_render($form['quotes']['quotes'])); - $commands[] = ajax_command_prepend('#quote', theme('status_messages')); - - return array('#type' => 'ajax', '#commands' => $commands); +function uc_quote_replace_order_quotes($form, $form_state) { + return $form['quotes']['quotes']; } /** diff --git a/uc_ajax_admin/uc_ajax_admin.info b/uc_ajax_admin/uc_ajax_admin.info new file mode 100644 index 0000000..edba546 --- /dev/null +++ b/uc_ajax_admin/uc_ajax_admin.info @@ -0,0 +1,5 @@ +name = Ubercart Ajax Administration +description = Administrative interface for ajax updates to Ubercart forms. +dependencies[] = uc_cart +package = Ubercart - extra +core = 7.x diff --git a/uc_ajax_admin/uc_ajax_admin.module b/uc_ajax_admin/uc_ajax_admin.module new file mode 100644 index 0000000..1ae9557 --- /dev/null +++ b/uc_ajax_admin/uc_ajax_admin.module @@ -0,0 +1,243 @@ + 'Ajax', + 'description' => 'Administer ajax updates on checkout form.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('uc_ajax_admin_form', 'checkout'), + 'access arguments' => array('administer store'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 3, + ); + return $items; +} + +/** + * Administration form for uc_ajax. + * + * @param $target_form + * The form for which ajax behaviors are to be administered. Currently only + * 'checkout' is supported. + */ +function uc_ajax_admin_form($form, &$form_state, $target_form = 'checkout') { + module_load_include('inc', 'uc_store', 'includes/uc_ajax_attach'); + switch ($target_form) { + case 'checkout': + $triggers = _uc_ajax_admin_checkout_trigger_options(_uc_ajax_admin_build_checkout_form()); + $panes = _uc_checkout_pane_list(); + $wrappers = array(); + foreach ($panes as $id => $pane) { + $wrappers["$id-pane"] = _uc_checkout_pane_data($id, 'title'); + } + break; + + default: + drupal_not_found(); + } + $form['#uc_ajax_target'] = $target_form; + $form['#uc_ajax_config'] = variable_get('uc_ajax_' . $target_form, _uc_ajax_defaults($target_form)); + + $instructions = '

' . t('Use this page to configure "Ajax" behaviors on the @target_form form. The table below + associates triggering form input elements with @target_form panes. The contents of each associated pane + will be refreshed whenever the customer clicks on or changes the triggering form element. For example, you + can cause the available payment methods to be refreshed when the customer changes their billing zone.', + array('@target_form' => $target_form)) + . '

'; + $instructions .= '

' . t("Note that the triggering elements you can choose are listed based on the + @target_form form as it would be displayed to you right now. For example, if none of your shipping methods + apply to the current cart contents, you won't see the shipping quote selection element. If you don't see + the form element you wish to use as a trigger, try adding some products to the shopping cart or otherwise + simulating the customer experience, and verify that those elements are present on the form itself.", + array('@target_form' => $target_form)) + . '

'; + + $form['instructions'] = array( + '#type' => 'item', + '#title' => t('Checkout form Ajax Behaviors'), + '#markup' => $instructions, + ); + $form['table'] = tapir_get_table('uc_ajax_admin_table', $triggers, $wrappers, $form['#uc_ajax_config']); + $form['actions'] = array( + '#type' => 'actions', + 'submit' => array( + '#type' => 'submit', + '#value' => t('Submit'), + ), + ); + return $form; +} + +/** + * Submit handler for the uc_ajax_admin form. + */ +function uc_ajax_admin_form_submit($form, &$form_state) { + $config = $form['#uc_ajax_config']; + foreach ($form_state['values']['table'] as $index => $entry) { + $key = $entry['key']; + if ($index === '_new') { + if (!empty($key) && !empty($entry['panes'])) { + $config[$key] = $entry['panes']; + } + } + elseif ($entry['remove'] || empty($entry['panes'])) { + unset($config[$key]); + } + else { + $config[$key] = $entry['panes']; + } + } + variable_set('uc_ajax_' . $form['#uc_ajax_target'], $config); + drupal_set_message(t('Your changes have been saved.')); +} + +/** + * TAPIr table callback for the uc_ajax administrative form. + * + * @param $trigger_options + * The select options for triggering elements. + * @param $wrapper_options + * The select options for wrappers. + * @param $config + * The existing configuration. + */ +function uc_ajax_admin_table($trigger_options, $wrapper_options, $config) { + $rows = array(); + foreach ($config as $key => $panes) { + list(, $pane) = explode('][', $key); + $rows[] = array( + 'key' => array( + '#type' => 'hidden', + '#value' => $key, + '#suffix' => empty($trigger_options[ucfirst($pane)][$key]) ? $key : ucfirst($pane) . ': ' . $trigger_options[ucfirst($pane)][$key], + ), + 'panes' => array( + '#type' => 'select', + '#options' => $wrapper_options, + '#default_value' => $panes, + '#multiple' => TRUE, + ), + 'remove' => array( + '#type' => 'checkbox', + '#default_value' => FALSE, + ), + ); + } + $rows['_new'] = array( + 'key' => array( + '#type' => 'select', + '#options' => array(0 => t('--Add a new element--')) + $trigger_options, + ), + 'panes' => array( + '#type' => 'select', + '#options' => $wrapper_options, + '#multiple' => TRUE, + ), + 'remove' => array( + '#type' => 'hidden', + '#value' => 0, + ), + ); + + $table = array( + '#type' => 'tapir_table', + '#tree' => TRUE, + '#columns' => array( + 'remove' => array( + 'cell' => t('Remove'), + 'weight' => 3, + ), + 'key' => array( + 'cell' => t('Triggering form element'), + 'weight' => 1, + ), + 'panes' => array( + 'cell' => t('Panes to update'), + 'weight' => 2, + ), + ), + ) + $rows; + + return $table; +} + +/** + * Recursively builds a list of all form elements which are suitable triggers + * for ajax updates. + * + * @param $element + * The element to check. + * @param $list + * The list being built. When complete will be an array of the form + * 'element_name' => 'Element title' + * where 'element_name' is the name of the element as would be specified for + * form_set_error(), and 'Element title' is a best guess at the human readable + * name of the element. + */ +function _uc_ajax_admin_list_triggers($element, &$list) { + if (!empty($element['#input']) && !in_array($element['#type'], array('hidden', 'uc_address'))) { + $key = implode('][', $element['#array_parents']); + switch ($element['#type']) { + case 'button': case 'submit': + $title = empty($element['#value']) ? $key : $element['#value']; + break; + default: + $title = empty($element['#title']) ? $key : $element['#title']; + } + $list[$key] = $title; + } + if (empty($element['#type']) || !in_array($element['#type'], array('radios', 'checkboxes'))) { + foreach (element_children($element) as $child) { + _uc_ajax_admin_list_triggers($element[$child], $list); + } + } +} + +/** + * Builds a hierarchical list of possible ajax triggers for the checkout form. + * + * @param $form + * The fully processed checkout form to search for triggers. + * + * @return + * An hierarchical array of select options, categorized by pane. + */ +function _uc_ajax_admin_checkout_trigger_options($form) { + $list = array(); + foreach (element_children($form['panes']) as $name) { + $group = ucfirst($name); + $list[$group] = array(); + _uc_ajax_admin_list_triggers($form['panes'][$name], $list[$group]); + if (empty($list[$group])) { + unset($list[$group]); + } + } + return $list; +} + +/** + * Builds the checkout form, using the cart order if it exists, or a default + * shippable order if not. + */ +function _uc_ajax_admin_build_checkout_form() { + module_load_include('inc', 'uc_cart', 'uc_cart.pages'); + $order = empty($_SESSION['cart_order']) ? FALSE : uc_order_load($_SESSION['cart_order']); + if (!$order) { + $order = new UcOrder(); + $order->products = array((object) array( + 'cart_item_id' => 0, + 'title' => 'fake', + 'nid' => 0, + 'qty' => 1, + 'price' => 1, + 'data' => array('shippable' => TRUE), + 'model' => 0, + 'weight' => 0 + )); + } + return drupal_get_form('uc_cart_checkout_form', $order); +} diff --git a/uc_cart/uc_cart.pages.inc b/uc_cart/uc_cart.pages.inc index 48c18cb..2c476d6 100644 --- a/uc_cart/uc_cart.pages.inc +++ b/uc_cart/uc_cart.pages.inc @@ -158,8 +158,9 @@ function uc_cart_checkout() { * @param $order * The order that is being checked out. * + * @see uc_cart_checkout_form_process() * @see uc_cart_checkout_form_validate() - * @see uc_cart_checkout_form_review() + * @see uc_cart_checkout_form_submit() * @see uc_cart_checkout_review() * @see theme_uc_cart_checkout_form() * @ingroup forms @@ -279,6 +280,9 @@ function uc_cart_checkout_form($form, &$form_state, $order) { '#value' => t('Review order'), ); + form_load_include($form_state, 'inc', 'uc_store', 'includes/uc_ajax_attach'); + $form['#process'][] = 'uc_ajax_process_form'; + unset($_SESSION['uc_checkout'][$order->order_id]); return $form; diff --git a/uc_order/uc_order.admin.inc b/uc_order/uc_order.admin.inc index 3cb5098..e0b1507 100644 --- a/uc_order/uc_order.admin.inc +++ b/uc_order/uc_order.admin.inc @@ -1058,6 +1058,9 @@ function uc_order_edit_form($form, &$form_state, $order) { field_attach_form('uc_order', $order, $form, $form_state); + form_load_include($form_state, 'inc', 'uc_store', 'includes/uc_ajax_attach'); + $form['#process'][] = 'uc_ajax_process_form'; + return $form; } diff --git a/uc_order/uc_order.order_pane.inc b/uc_order/uc_order.order_pane.inc index bd1eb48..e4e1d1b 100644 --- a/uc_order/uc_order.order_pane.inc +++ b/uc_order/uc_order.order_pane.inc @@ -583,13 +583,13 @@ function uc_order_edit_products_remove($form, &$form_state) { * AJAX callback to render the order product controls. */ function uc_order_pane_products_ajax_callback($form, &$form_state) { - $commands[] = ajax_command_replace('#product-controls', drupal_render($form['product_controls'])); - $commands[] = ajax_command_prepend('#product-controls', theme('status_messages')); + $commands[] = ajax_command_replace('#product-controls', trim(drupal_render($form['product_controls']))); + $commands[] = ajax_command_prepend('#product-controls', trim(theme('status_messages'))); if (isset($form_state['refresh_products']) && $form_state['refresh_products']) { - $commands[] = ajax_command_replace('#order-edit-products', drupal_render($form['products'])); - $commands[] = ajax_command_replace('#order-line-items', drupal_render($form['line_items'])); - $commands[] = ajax_command_prepend('#order-edit-products', theme('status_messages')); + $commands[] = ajax_command_replace('#order-edit-products', trim(drupal_render($form['products']))); + $commands[] = ajax_command_replace('#order-line-items', trim(drupal_render($form['line_items']))); + $commands[] = ajax_command_prepend('#order-edit-products', trim(theme('status_messages'))); } return array('#type' => 'ajax', '#commands' => $commands); @@ -816,7 +816,7 @@ function uc_order_pane_line_items_remove($form, &$form_state) { * AJAX callback to render the line items. */ function uc_order_pane_line_items_update($form, &$form_state) { - $commands[] = ajax_command_replace('#order-line-items', drupal_render($form['line_items'])); + $commands[] = ajax_command_replace('#order-line-items', trim(drupal_render($form['line_items']))); $commands[] = ajax_command_prepend('#order-line-items', theme('status_messages')); return array('#type' => 'ajax', '#commands' => $commands); diff --git a/uc_store/includes/uc_ajax_attach.inc b/uc_store/includes/uc_ajax_attach.inc new file mode 100644 index 0000000..973721a --- /dev/null +++ b/uc_store/includes/uc_ajax_attach.inc @@ -0,0 +1,231 @@ + 'uc_ajax_replace_checkout_pane', + * ); + * @endcode + * + * This will cause the contents of 'quotes-pane' to be replaced by the return + * value of uc_ajax_replace_checkout_pane(). Note that if more than one module + * assign a callback to the same wrapper key, the heavier module or pane will + * take precedence. + * + * Implementors need not provide a wrapper key for each callback, in which case + * the callback must return an array of ajax commands rather than a renderable + * form element. For example: + * + * @code + * $form_state['uc_ajax']['mymodule']['panes][quotes][quote_button'] = array('my_ajax_callback'); + * ... + * function my_ajax_callback($form, $form_state) { + * $commands[] = ajax_command_invoke('#my-input-element', 'val', 0); + * return array('#type' => 'ajax', '#commands' => $commands); + * } + * @endcode + * + * However, using a wrapper key where appropriate will reduce redundant + * replacements of the same element. + * + * NOTE: 'uc_ajax_replace_checkout_pane' is a convenience callback which will + * replace the contents of an entire checkout pane. It is generally preferable + * to use this when updating data on the checkout form, as this will + * further reduce the likelihood of redundant replacements. You should use + * your own callback only when behaviours other than replacement are + * desired, or when replacing data that lie outside a checkout pane. Note + * also that you may combine both formulations by mixing numeric and string keys. + * For example: + * + * @code + * $form_state['uc_ajax']['mymodule']['panes][quotes][quote_button'] = array( + * 'my_ajax_callback', + * 'quotes-pane' => 'uc_ajax_replace_checkout_pane', + * ); + * @endcode + */ + +/** + * Form process callback to allow multiple Ajax callbacks on form elements. + */ +function uc_ajax_process_form($form, &$form_state) { + // When processing the top level form, add any variable-defined pane wrappers. + if (isset($form['#form_id'])) { + switch ($form['#form_id']) { + case 'uc_cart_checkout_form': + $config = variable_get('uc_ajax_checkout', _uc_ajax_defaults('checkout')); + foreach ($config as $key => $panes) { + foreach (array_keys($panes) as $pane) { + $config[$key][$pane] = 'uc_ajax_replace_checkout_pane'; + } + } + $form_state['uc_ajax']['uc_ajax'] = $config; + break; + } + } + + if (!isset($form_state['uc_ajax'])) { + return $form; + } + + // We have to operate on the children rather than on the element itself, as + // #process functions are called *after* form_handle_input_elements(), + // which is where the triggering element is determined. If we haven't added + // an '#ajax' key by that time, Drupal won't be able to determine which + // callback to invoke. + foreach (element_children($form) as $child) { + $element =& $form[$child]; + + // Add this process function recursively to the children. + if (empty($element['#process']) && !empty($element['#type'])) { + // We want to be sure the default process functions for the element type are called. + $info = element_info($element['#type']); + if (!empty($info['#process'])) { + $element['#process'] = $info['#process']; + } + } + $element['#process'][] = 'uc_ajax_process_form'; + + // Multiplex any Ajax calls for this element. + $parents = $form['#array_parents']; + array_push($parents, $child); + $key = implode('][', $parents); + + $callbacks = array(); + foreach ($form_state['uc_ajax'] as $module => $fields) { + if (!empty($fields[$key])) { + if (is_array($fields[$key])) { + $callbacks = array_merge($callbacks, $fields[$key]); + } + else { + $callbacks[] = $fields[$key]; + } + } + } + + if (!empty($callbacks)) { + if (empty($element['#ajax'])) { + $element['#ajax'] = array(); + } + elseif (!empty($element['#ajax']['callback'])) { + if (!empty($element['#ajax']['wrapper'])) { + $callbacks[$element['#ajax']['wrapper']] = $element['#ajax']['callback']; + } + else { + array_unshift($callbacks, $element['#ajax']['callback']); + } + } + + $element['#ajax'] = array_merge($element['#ajax'], array( + 'callback' => 'uc_ajax_multiplex', + 'list' => $callbacks, + )); + } + } + + return $form; +} + +/** + * Ajax callback multiplexer. + * + * Processes a set of Ajax commands attached to the triggering element. + */ +function uc_ajax_multiplex($form, $form_state) { + $element = $form_state['triggering_element']; + if (!empty($element['#ajax']['list'])) { + $commands = array(); + foreach ($element['#ajax']['list'] as $wrapper => $callback) { + if (!empty($callback) && function_exists($callback) && $result = $callback($form, $form_state, $wrapper)) { + if (is_array($result) && !empty($result['#type']) && $result['#type'] == 'ajax') { + // If the callback returned an array of commands, simply add these to the list. + $commands = array_merge($commands, $result['#commands']); + } + elseif (is_string($wrapper)) { + // Otherwise, assume the callback returned a string or render-array, and insert it into the wrapper. + $html = is_string($result) ? $result : drupal_render($result); + $commands[] = ajax_command_replace('#' . $wrapper, trim($html)); + $commands[] = ajax_command_prepend('#' . $wrapper, theme('status_messages')); + } + } + } + } + if (!empty($commands)) { + return array('#type' => 'ajax', '#commands' => $commands); + } +} + +/** + * Ajax callback to replace a whole checkout pane. + * + * @param $form + * The checkout form. + * @param $form_state + * The current form state. + * @param $wrapper + * Special third parameter passed for uc_ajax callbacks containing the ajax + * wrapper for this callback. Here used to determine which pane to replace. + * + * @return + * The form element representing the pane, suitable for ajax rendering. If + * the pane does not exist, or if the wrapper does not refer to a checkout + * pane, returns nothing. + */ +function uc_ajax_replace_checkout_pane($form, $form_state, $wrapper = NULL) { + if (empty($wrapper) && !empty($form_state['triggering_element']['#ajax']['wrapper'])) { + // If $wrapper is absent, then we were not invoked by uc_ajax_multiplex, + // so try to use the wrapper of the triggering element's #ajax array. + $wrapper = $form_state['triggering_element']['#ajax']['wrapper']; + } + if (!empty($wrapper)) { + list($pane, $verify) = explode('-', $wrapper); + if ($verify === 'pane' && !empty($form['panes'][$pane])) { + return $form['panes'][$pane]; + } + } +} + +/** + * Retrieve the default ajax behaviors for a target form. + * + * @param $target_form + * The form whose default behaviors are to be retrieved. + * + * @return + * The array of default behaviors for the form. + */ +function _uc_ajax_defaults($target_form) { + switch ($target_form) { + case 'checkout': + $quotes_defaults = drupal_map_assoc(array('payment-pane', 'quotes-pane')); + return array( + 'panes][delivery][address][delivery_country' => $quotes_defaults, + 'panes][delivery][address][delivery_postal_code' => $quotes_defaults, + 'panes][delivery][select_address' => $quotes_defaults, + 'panes][billing][address][billing_country' => array('payment-pane' => 'payment-pane'), + ); + default: + return array(); + } +} diff --git a/uc_store/tests/uc_ajax.test b/uc_store/tests/uc_ajax.test new file mode 100644 index 0000000..dbbf8a3 --- /dev/null +++ b/uc_store/tests/uc_ajax.test @@ -0,0 +1,103 @@ + 'Ajax functionality', + 'description' => 'Ajax update of checkout and order pages.', + 'group' => 'Ubercart', + ); + } + + /** + * Overrides DrupalWebTestCase::setUp(). + */ + public function setUp() { + module_load_include('inc', 'uc_store', 'includes/uc_ajax_attach'); + $modules = array('rules_admin', 'uc_payment', 'uc_payment_pack'); + $permissions = array('administer rules', 'bypass rules access'); + parent::setUp($modules, $permissions); + $this->drupalLogin($this->adminUser); + } + + /** + * Set a zone-based condition for a particular payment method. + * + * @param $method + * The method to set (e.g. 'check') + * @param $zone + * The zone id (numeric) to check for. + * @param $negate + * TRUE to negate the condition. + */ + function addPaymentZoneCondition($method, $zone, $negate = FALSE) { + $not = $negate ? 'NOT ' : ''; + $name = 'uc_payment_method_' . $method; + $label = ucfirst($method) . ' conditions'; + $condition = array( + 'LABEL' => $label, + 'PLUGIN' => 'and', + 'REQUIRES' => array('rules'), + 'USES VARIABLES' => array( + 'order' => array( + 'label' => 'Order', + 'type' => 'uc_order', + ), + ), + 'AND' => array( + array( + $not . 'data_is' => array( + 'data' => array('order:billing-address:zone'), + 'value' => $zone, + ), + ), + ), + ); + $newconfig = rules_import(array($name => $condition)); + $oldconfig = rules_config_load($name); + if ($oldconfig) { + $newconfig->id = $oldconfig->id; + unset($newconfig->is_new); + $newconfig->status = ENTITY_CUSTOM; + } + $newconfig->save(); + entity_flush_caches(); + //$this->drupalGet('admin/config/workflow/rules/components/manage/' . $newconfig->id); + } + + function testCheckoutAjax() { + // Enable two payment methods and set a condition on one. + variable_set('uc_payment_method_check_checkout', TRUE); + variable_set('uc_payment_method_other_checkout', TRUE); + $this->addPaymentZoneCondition('other', '26'); + + // Speciy that the billing zone should update the payment pane. + $config = variable_get('uc_ajax_checkout', _uc_ajax_defaults('checkout')); + $config['panes][billing][address][billing_zone'] = array('payment-pane' => 'payment-pane'); + variable_set('uc_ajax_checkout', $config); + + // Go to the checkout page, veriy that the conditional payment method is + // not available. + $product = $this->createProduct(array('shippable' => FALSE)); + $this->drupalPost('node/' . $product->nid, array(), t('Add to cart')); + $this->drupalPost('cart', array('items[0][qty]' => 1), t('Checkout')); + $this->assertNoText('Other'); + + // Change the billing zone and veriy that payment pane updates. + $edit = array(); + $edit['panes[billing][billing_zone]'] = '26'; + $result = $this->ucPostAjax(NULL, $edit, 'panes[billing][billing_zone]'); + $this->assertText("Other"); + $edit['panes[billing][billing_zone]'] = '1'; + $result = $this->ucPostAjax(NULL, $edit, 'panes[billing][billing_zone]'); + // Not in Kansas any more... + $this->assertNoText("Other"); + } +} diff --git a/uc_store/uc_store.info b/uc_store/uc_store.info index 0cac244..3bfb047 100644 --- a/uc_store/uc_store.info +++ b/uc_store/uc_store.info @@ -11,6 +11,7 @@ files[] = classes/encrypt.inc ; Test cases files[] = tests/uc_store.test files[] = tests/uc_address.test +files[] = tests/uc_ajax.test configure = admin/store/settings/store stylesheets[all][] = uc_store.css