| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 | <?php/** * @file * CTools' multi-step form wizard tool. * * This tool enables the creation of multi-step forms that go from one * form to another. The forms themselves can allow branching if they * like, and there are a number of configurable options to how * the wizard operates. * * The wizard can also be friendly to ajax forms, such as when used * with the modal tool. * * The wizard provides callbacks throughout the process, allowing the * owner to control the flow. The general flow of what happens is: * * Generate a form * submit a form * based upon button clicked, 'finished', 'next form', 'cancel' or 'return'. * * Each action has its own callback, so cached objects can be modifed and or * turned into real objects. Each callback can make decisions about where to * go next if it wishes to override the default flow. *//** * Display a multi-step form. * * Aside from the addition of the $form_info which contains an array of * information and configuration so the multi-step wizard can do its thing, * this function works a lot like drupal_build_form. * * Remember that the form builders for this form will receive * &$form, &$form_state, NOT just &$form_state and no additional args. * * @param $form_info *   An array of form info. @todo document the array. * @param $step *   The current form step. * @param &$form_state *   The form state array; this is a reference so the caller can get back *   whatever information the form(s) involved left for it. */function ctools_wizard_multistep_form($form_info, $step, &$form_state) {  // Make sure 'wizard' always exists for the form when dealing  // with form caching.  ctools_form_include($form_state, 'wizard');  // allow order array to be optional  if (empty($form_info['order'])) {    foreach ($form_info['forms'] as $step_id => $params) {      $form_info['order'][$step_id] = $params['title'];    }  }  if (!isset($step)) {    $keys = array_keys($form_info['order']);    $step = array_shift($keys);  }  ctools_wizard_defaults($form_info);  // If automated caching is enabled, ensure that everything is as it  // should be.  if (!empty($form_info['auto cache'])) {    // If the cache mechanism hasn't been set, default to the simple    // mechanism and use the wizard ID to ensure uniqueness so cache    // objects don't stomp on each other.    if (!isset($form_info['cache mechanism'])) {      $form_info['cache mechanism'] = 'simple::wizard::' . $form_info['id'];    }    // If not set, default the cache key to the wizard ID. This is often    // a unique ID of the object being edited or similar.    if (!isset($form_info['cache key'])) {      $form_info['cache key'] = $form_info['id'];    }    // If not set, default the cache location to storage. This is often    // somnething like 'conf'.    if (!isset($form_info['cache location'])) {      $form_info['cache location'] = 'storage';    }    // If absolutely nothing was set for the cache area to work on    if (!isset($form_state[$form_info['cache location']])) {      ctools_include('cache');      $form_state[$form_info['cache location']] = ctools_cache_get($form_info['cache mechanism'], $form_info['cache key']);    }  }  $form_state['step'] = $step;  $form_state['form_info'] = $form_info;  // Ensure we have form information for the current step.  if (!isset($form_info['forms'][$step])) {    return;  }  // Ensure that whatever include file(s) were requested by the form info are  // actually included.  $info = $form_info['forms'][$step];  if (!empty($info['include'])) {    if (is_array($info['include'])) {      foreach ($info['include'] as $file) {        ctools_form_include_file($form_state, $file);      }    }    else {      ctools_form_include_file($form_state, $info['include']);    }  }  // This tells drupal_build_form to apply our wrapper to the form. It  // will give it buttons and the like.  $form_state['wrapper_callback'] = 'ctools_wizard_wrapper';  if (!isset($form_state['rerender'])) {    $form_state['rerender'] = FALSE;  }  $form_state['no_redirect'] = TRUE;  $output = drupal_build_form($info['form id'], $form_state);  if (empty($form_state['executed']) || !empty($form_state['rerender'])) {    if (empty($form_state['title']) && !empty($info['title'])) {      $form_state['title'] = $info['title'];    }    if (!empty($form_state['ajax render'])) {      // Any include files should already be included by this point:      return $form_state['ajax render']($form_state, $output);    }    // Automatically use the modal tool if set to true.    if (!empty($form_state['modal']) && empty($form_state['modal return'])) {      ctools_include('modal');      // This overwrites any previous commands.      $form_state['commands'] = ctools_modal_form_render($form_state, $output);    }  }  if (!empty($form_state['executed'])) {    // We use the plugins get_function format because it's powerful and    // not limited to just functions.    ctools_include('plugins');    if (isset($form_state['clicked_button']['#wizard type'])) {      $type = $form_state['clicked_button']['#wizard type'];      // If we have a callback depending upon the type of button that was      // clicked, call it.      if ($function = ctools_plugin_get_function($form_info, "$type callback")) {        $function($form_state);      }      // If auto-caching is on, we need to write the cache on next and      // clear the cache on finish.      if (!empty($form_info['auto cache'])) {        if ($type == 'next') {          ctools_include('cache');          ctools_cache_set($form_info['cache mechanism'], $form_info['cache key'], $form_state[$form_info['cache location']]);        }        elseif ($type == 'finish') {          ctools_include('cache');          ctools_cache_clear($form_info['cache mechanism'], $form_info['cache key']);        }      }      // Set a couple of niceties:      if ($type == 'finish') {        $form_state['complete'] = TRUE;      }      if ($type == 'cancel') {        $form_state['cancel'] = TRUE;      }      // If the modal is in use, some special code for it:      if (!empty($form_state['modal']) && empty($form_state['modal return'])) {        if ($type != 'next') {          // Automatically dismiss the modal if we're not going to another form.          ctools_include('modal');          $form_state['commands'][] = ctools_modal_command_dismiss();        }      }    }    if (empty($form_state['ajax'])) {      // redirect, if one is set.      if ($form_state['redirect']) {        if (is_array($form_state['redirect'])) {          call_user_func_array('drupal_goto', $form_state['redirect']);        }        else {          drupal_goto($form_state['redirect']);        }      }    }    else if (isset($form_state['ajax next'])) {      // Clear a few items off the form state so we don't double post:      $next = $form_state['ajax next'];      unset($form_state['ajax next']);      unset($form_state['executed']);      unset($form_state['post']);      unset($form_state['next']);      return ctools_wizard_multistep_form($form_info, $next, $form_state);    }    // If the callbacks wanted to do something besides go to the next form,    // it needs to have set $form_state['commands'] with something that can    // be rendered.  }  // Render ajax commands if we have any.  if (isset($form_state['ajax']) && isset($form_state['commands']) && empty($form_state['modal return'])) {    return ajax_render($form_state['commands']);  }  // Otherwise, return the output.  return $output;}/** * Provide a wrapper around another form for adding multi-step information. */function ctools_wizard_wrapper($form, &$form_state) {  $form_info = &$form_state['form_info'];  $info = $form_info['forms'][$form_state['step']];  // Determine the next form from this step.  // Create a form trail if we're supposed to have one.  $trail = array();  $previous = TRUE;  foreach ($form_info['order'] as $id => $title) {    if ($id == $form_state['step']) {      $previous = FALSE;      $class = 'wizard-trail-current';    }    elseif ($previous) {      $not_first = TRUE;      $class = 'wizard-trail-previous';      $form_state['previous'] = $id;    }    else {      $class = 'wizard-trail-next';      if (!isset($form_state['next'])) {        $form_state['next'] = $id;      }      if (empty($form_info['show trail'])) {        break;      }    }    if (!empty($form_info['show trail'])) {      if (!empty($form_info['free trail'])) {        // ctools_wizard_get_path() returns results suitable for        // $form_state['redirect] which can only be directly used in        // drupal_goto. We have to futz a bit with it.        $path = ctools_wizard_get_path($form_info, $id);        $options = array();        if (!empty($path[1])) {          $options = $path[1];        }        $title = l($title, $path[0], $options);      }      $trail[] = '<span class="' . $class . '">' . $title . '</span>';    }  }  // Display the trail if instructed to do so.  if (!empty($form_info['show trail'])) {    ctools_add_css('wizard');    $form['ctools_trail'] = array(      '#markup' => theme(array('ctools_wizard_trail__' . $form_info['id'], 'ctools_wizard_trail'), array('trail' => $trail, 'form_info' => $form_info)),      '#weight' => -1000,    );  }  if (empty($form_info['no buttons'])) {    // Ensure buttons stay on the bottom.    $form['buttons'] = array(      '#type' => 'actions',      '#weight' => 1000,    );    $button_attributes = array();    if (!empty($form_state['ajax']) && empty($form_state['modal'])) {      $button_attributes = array('class' => array('ctools-use-ajax'));    }    if (!empty($form_info['show back']) && isset($form_state['previous'])) {      $form['buttons']['previous'] = array(        '#type' => 'submit',        '#value' => $form_info['back text'],        '#next' => $form_state['previous'],        '#wizard type' => 'next',        '#weight' => -2000,        '#limit_validation_errors' => array(),        // hardcode the submit so that it doesn't try to save data.        '#submit' => array('ctools_wizard_submit'),        '#attributes' => $button_attributes,      );      if (isset($form_info['no back validate']) || isset($info['no back validate'])) {        $form['buttons']['previous']['#validate'] = array();      }    }    // If there is a next form, place the next button.    if (isset($form_state['next']) || !empty($form_info['free trail'])) {      $form['buttons']['next'] = array(        '#type' => 'submit',        '#value' => $form_info['next text'],        '#next' => !empty($form_info['free trail']) ? $form_state['step'] : $form_state['next'],        '#wizard type' => 'next',        '#weight' => -1000,        '#attributes' => $button_attributes,      );    }    // There are two ways the return button can appear. If this is not the    // end of the form list (i.e, there is a next) then it's "update and return"    // to be clear. If this is the end of the path and there is no next, we    // call it 'Finish'.    // Even if there is no direct return path (some forms may not want you    // leaving in the middle) the final button is always a Finish and it does    // whatever the return action is.    if (!empty($form_info['show return']) && !empty($form_state['next'])) {      $form['buttons']['return'] = array(        '#type' => 'submit',        '#value' =>  $form_info['return text'],        '#wizard type' => 'return',        '#attributes' => $button_attributes,      );    }    else if (empty($form_state['next']) || !empty($form_info['free trail'])) {      $form['buttons']['return'] = array(        '#type' => 'submit',        '#value' => $form_info['finish text'],        '#wizard type' => 'finish',        '#attributes' => $button_attributes,      );    }    // If we are allowed to cancel, place a cancel button.    if ((isset($form_info['cancel path']) && !isset($form_info['show cancel'])) || !empty($form_info['show cancel'])) {      $form['buttons']['cancel'] = array(        '#type' => 'submit',        '#value' => $form_info['cancel text'],        '#wizard type' => 'cancel',        // hardcode the submit so that it doesn't try to save data.        '#limit_validation_errors' => array(),        '#submit' => array('ctools_wizard_submit'),        '#attributes' => $button_attributes,      );    }    // Set up optional validate handlers.    $form['#validate'] = array();    if (function_exists($info['form id'] . '_validate')) {      $form['#validate'][] = $info['form id'] . '_validate';    }    if (isset($info['validate']) && function_exists($info['validate'])) {      $form['#validate'][] = $info['validate'];    }    // Set up our submit handler after theirs. Since putting something here will    // skip Drupal's autodetect, we autodetect for it.    // We make sure ours is after theirs so that they get to change #next if    // the want to.    $form['#submit'] = array();    if (function_exists($info['form id'] . '_submit')) {      $form['#submit'][] = $info['form id'] . '_submit';    }    if (isset($info['submit']) && function_exists($info['submit'])) {      $form['#submit'][] = $info['submit'];    }    $form['#submit'][] = 'ctools_wizard_submit';  }  if (!empty($form_state['ajax'])) {    $params = ctools_wizard_get_path($form_state['form_info'], $form_state['step']);    if (count($params) > 1) {      $url = array_shift($params);      $options = array();      $keys = array(0 => 'query', 1 => 'fragment');      foreach ($params as $key => $value) {        if (isset($keys[$key]) && isset($value)) {          $options[$keys[$key]] = $value;        }      }      $params = array($url, $options);    }    $form['#action'] =  call_user_func_array('url', $params);  }  if (isset($info['wrapper']) && function_exists($info['wrapper'])) {    $form = $info['wrapper']($form, $form_state);  }  if (isset($form_info['wrapper']) && function_exists($form_info['wrapper'])) {    $form = $form_info['wrapper']($form, $form_state);  }  return $form;}/** * On a submit, go to the next form. */function ctools_wizard_submit(&$form, &$form_state) {  if (isset($form_state['clicked_button']['#wizard type'])) {    $type = $form_state['clicked_button']['#wizard type'];    // if AJAX enabled, we proceed slightly differently here.    if (!empty($form_state['ajax'])) {      if ($type == 'next') {        $form_state['ajax next'] = $form_state['clicked_button']['#next'];      }    }    else {      if ($type == 'cancel' && isset($form_state['form_info']['cancel path'])) {        $form_state['redirect'] = $form_state['form_info']['cancel path'];      }      else if ($type == 'next') {        $form_state['redirect'] = ctools_wizard_get_path($form_state['form_info'], $form_state['clicked_button']['#next']);        if (!empty($_GET['destination'])) {          // We don't want drupal_goto redirect this request          // back. ctools_wizard_get_path ensures that the destination is          // carried over on subsequent pages.          unset($_GET['destination']);        }      }      else if (isset($form_state['form_info']['return path'])) {        $form_state['redirect'] = $form_state['form_info']['return path'];      }      else if ($type == 'finish' && isset($form_state['form_info']['cancel path'])) {        $form_state['redirect'] = $form_state['form_info']['cancel path'];      }    }  }}/** * Create a path from the form info and a given step. */function ctools_wizard_get_path($form_info, $step) {  if (is_array($form_info['path'])) {    foreach ($form_info['path'] as $id => $part) {      $form_info['path'][$id] = str_replace('%step', $step, $form_info['path'][$id]);    }    $path = $form_info['path'];  }  else {    $path = array(str_replace('%step', $step, $form_info['path']));  }  // If destination is set, carry it over so it'll take effect when  // saving. The submit handler will unset destination to avoid drupal_goto  // redirecting us.  if (!empty($_GET['destination'])) {    // Ensure that options is an array.    if (!isset($path[1]) || !is_array($path[1])) {      $path[1] = array();    }    // Ensure that the query part of options is an array.    $path[1] += array('query' => array());    // Add the destination parameter, if not set already.    $path[1]['query'] += drupal_get_destination();  }  return $path;}/** * Set default parameters and callbacks if none are given. * Callbacks follows pattern: * $form_info['id']_$hook * $form_info['id']_$form_info['forms'][$step_key]_$hook */function ctools_wizard_defaults(&$form_info) {  $hook = $form_info['id'];  $defaults = array(    'show trail' => FALSE,    'free trail' => FALSE,    'show back' => FALSE,    'show cancel' => FALSE,    'show return' => FALSE,    'next text' => t('Continue'),    'back text' => t('Back'),    'return text' => t('Update and return'),    'finish text' => t('Finish'),    'cancel text' => t('Cancel'),  );  if (!empty($form_info['free trail'])) {    $defaults['next text'] = t('Update');    $defaults['finish text'] = t('Save');  }  $form_info = $form_info + $defaults;  // set form callbacks if they aren't defined  foreach ($form_info['forms'] as $step => $params) {    if (!$params['form id']) {       $form_callback = $hook . '_' . $step . '_form';       $form_info['forms'][$step]['form id'] = $form_callback;    }  }  // set button callbacks  $callbacks = array(    'back callback' => '_back',    'next callback' => '_next',    'return callback' => '_return',    'cancel callback' => '_cancel',    'finish callback' => '_finish',  );  foreach ($callbacks as $key => $callback) {    // never overwrite if explicity defined    if (empty($form_info[$key])) {      $wizard_callback = $hook . $callback;      if (function_exists($wizard_callback))  {        $form_info[$key] = $wizard_callback;      }    }  }}
 |