TRUE with these forms as that validation
* cannot be skipped for the CANCEL button.
*
* @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) {
// 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);
$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) {
require_once './' . $file;
}
}
else {
require_once './' . $info['include'];
}
}
// This tells ctools_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;
ctools_include('form');
$output = ctools_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 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']) {
return drupal_redirect_form(array(), $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']) && !empty($form_state['commands'])) {
return ctools_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 #redirect
// which can only be directly used in drupal_goto. We have to futz
// with it.
$path = ctools_wizard_get_path($form_info, $id);
$options = array();
if (!empty($path[1])) {
$options['query'] = $path[1];
}
if (!empty($path[2])) {
$options['fragment'] = $path[2];
}
$title = l($title, $path[0], $options);
}
$trail[] = '' . $title . '';
}
}
// Display the trail if instructed to do so.
if (!empty($form_info['show trail'])) {
ctools_add_css('wizard');
$form['ctools_trail'] = array(
'#value' => theme(array('ctools_wizard_trail__' . $form_info['id'], 'ctools_wizard_trail'), $trail),
'#weight' => -1000,
);
}
if (empty($form_info['no buttons'])) {
// Ensure buttons stay on the bottom.
$form['buttons'] = array(
'#prefix' => '
',
'#suffix' => '
',
'#weight' => 1000,
);
$button_attributes = array();
if (!empty($form_state['ajax']) && empty($form_state['modal'])) {
$button_attributes = array('class' => '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,
'#skip validation' => TRUE,
// 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.
'#skip validation' => TRUE,
'#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'])) {
$info['wrapper']($form, $form_state);
}
if (isset($form_info['wrapper']) && function_exists($form_info['wrapper'])) {
$form_info['wrapper']($form, $form_state);
}
}
/**
* 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']);
}
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]);
}
return $form_info['path'];
}
else {
return array(str_replace('%step', $step, $form_info['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;
}
}
}
}