123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- <?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']);
- }
- }
- }
- elseif (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,
- );
- }
- elseif (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'];
- }
- elseif ($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']);
- }
- }
- elseif (isset($form_state['form_info']['return path'])) {
- $form_state['redirect'] = $form_state['form_info']['return path'];
- }
- elseif ($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();
- }
- // Add the destination parameter, if not set already.
- $path[1] += 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 (empty($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;
- }
- }
- }
- }
|