comparison sites/all/modules/ctools/includes/wizard.inc @ 0:ff03f76ab3fe

initial version
author danieleb <danielebarchiesi@me.com>
date Wed, 21 Aug 2013 18:51:11 +0100
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:ff03f76ab3fe
1 <?php
2
3 /**
4 * @file
5 * CTools' multi-step form wizard tool.
6 *
7 * This tool enables the creation of multi-step forms that go from one
8 * form to another. The forms themselves can allow branching if they
9 * like, and there are a number of configurable options to how
10 * the wizard operates.
11 *
12 * The wizard can also be friendly to ajax forms, such as when used
13 * with the modal tool.
14 *
15 * The wizard provides callbacks throughout the process, allowing the
16 * owner to control the flow. The general flow of what happens is:
17 *
18 * Generate a form
19 * submit a form
20 * based upon button clicked, 'finished', 'next form', 'cancel' or 'return'.
21 *
22 * Each action has its own callback, so cached objects can be modifed and or
23 * turned into real objects. Each callback can make decisions about where to
24 * go next if it wishes to override the default flow.
25 */
26
27 /**
28 * Display a multi-step form.
29 *
30 * Aside from the addition of the $form_info which contains an array of
31 * information and configuration so the multi-step wizard can do its thing,
32 * this function works a lot like drupal_build_form.
33 *
34 * Remember that the form builders for this form will receive
35 * &$form, &$form_state, NOT just &$form_state and no additional args.
36 *
37 * @param $form_info
38 * An array of form info. @todo document the array.
39 * @param $step
40 * The current form step.
41 * @param &$form_state
42 * The form state array; this is a reference so the caller can get back
43 * whatever information the form(s) involved left for it.
44 */
45 function ctools_wizard_multistep_form($form_info, $step, &$form_state) {
46 // Make sure 'wizard' always exists for the form when dealing
47 // with form caching.
48 ctools_form_include($form_state, 'wizard');
49
50 // allow order array to be optional
51 if (empty($form_info['order'])) {
52 foreach ($form_info['forms'] as $step_id => $params) {
53 $form_info['order'][$step_id] = $params['title'];
54 }
55 }
56
57 if (!isset($step)) {
58 $keys = array_keys($form_info['order']);
59 $step = array_shift($keys);
60 }
61
62 ctools_wizard_defaults($form_info);
63
64 // If automated caching is enabled, ensure that everything is as it
65 // should be.
66 if (!empty($form_info['auto cache'])) {
67 // If the cache mechanism hasn't been set, default to the simple
68 // mechanism and use the wizard ID to ensure uniqueness so cache
69 // objects don't stomp on each other.
70 if (!isset($form_info['cache mechanism'])) {
71 $form_info['cache mechanism'] = 'simple::wizard::' . $form_info['id'];
72 }
73
74 // If not set, default the cache key to the wizard ID. This is often
75 // a unique ID of the object being edited or similar.
76 if (!isset($form_info['cache key'])) {
77 $form_info['cache key'] = $form_info['id'];
78 }
79
80 // If not set, default the cache location to storage. This is often
81 // somnething like 'conf'.
82 if (!isset($form_info['cache location'])) {
83 $form_info['cache location'] = 'storage';
84 }
85
86 // If absolutely nothing was set for the cache area to work on
87 if (!isset($form_state[$form_info['cache location']])) {
88 ctools_include('cache');
89 $form_state[$form_info['cache location']] = ctools_cache_get($form_info['cache mechanism'], $form_info['cache key']);
90 }
91 }
92
93 $form_state['step'] = $step;
94 $form_state['form_info'] = $form_info;
95
96 // Ensure we have form information for the current step.
97 if (!isset($form_info['forms'][$step])) {
98 return;
99 }
100
101 // Ensure that whatever include file(s) were requested by the form info are
102 // actually included.
103 $info = $form_info['forms'][$step];
104
105 if (!empty($info['include'])) {
106 if (is_array($info['include'])) {
107 foreach ($info['include'] as $file) {
108 ctools_form_include_file($form_state, $file);
109 }
110 }
111 else {
112 ctools_form_include_file($form_state, $info['include']);
113 }
114 }
115
116 // This tells drupal_build_form to apply our wrapper to the form. It
117 // will give it buttons and the like.
118 $form_state['wrapper_callback'] = 'ctools_wizard_wrapper';
119 if (!isset($form_state['rerender'])) {
120 $form_state['rerender'] = FALSE;
121 }
122
123 $form_state['no_redirect'] = TRUE;
124
125 $output = drupal_build_form($info['form id'], $form_state);
126
127 if (empty($form_state['executed']) || !empty($form_state['rerender'])) {
128 if (empty($form_state['title']) && !empty($info['title'])) {
129 $form_state['title'] = $info['title'];
130 }
131
132 if (!empty($form_state['ajax render'])) {
133 // Any include files should already be included by this point:
134 return $form_state['ajax render']($form_state, $output);
135 }
136
137 // Automatically use the modal tool if set to true.
138 if (!empty($form_state['modal']) && empty($form_state['modal return'])) {
139 ctools_include('modal');
140
141 // This overwrites any previous commands.
142 $form_state['commands'] = ctools_modal_form_render($form_state, $output);
143 }
144 }
145
146 if (!empty($form_state['executed'])) {
147 // We use the plugins get_function format because it's powerful and
148 // not limited to just functions.
149 ctools_include('plugins');
150
151 if (isset($form_state['clicked_button']['#wizard type'])) {
152 $type = $form_state['clicked_button']['#wizard type'];
153 // If we have a callback depending upon the type of button that was
154 // clicked, call it.
155 if ($function = ctools_plugin_get_function($form_info, "$type callback")) {
156 $function($form_state);
157 }
158
159 // If auto-caching is on, we need to write the cache on next and
160 // clear the cache on finish.
161 if (!empty($form_info['auto cache'])) {
162 if ($type == 'next') {
163 ctools_include('cache');
164 ctools_cache_set($form_info['cache mechanism'], $form_info['cache key'], $form_state[$form_info['cache location']]);
165 }
166 elseif ($type == 'finish') {
167 ctools_include('cache');
168 ctools_cache_clear($form_info['cache mechanism'], $form_info['cache key']);
169 }
170 }
171
172 // Set a couple of niceties:
173 if ($type == 'finish') {
174 $form_state['complete'] = TRUE;
175 }
176
177 if ($type == 'cancel') {
178 $form_state['cancel'] = TRUE;
179 }
180
181 // If the modal is in use, some special code for it:
182 if (!empty($form_state['modal']) && empty($form_state['modal return'])) {
183 if ($type != 'next') {
184 // Automatically dismiss the modal if we're not going to another form.
185 ctools_include('modal');
186 $form_state['commands'][] = ctools_modal_command_dismiss();
187 }
188 }
189 }
190
191 if (empty($form_state['ajax'])) {
192 // redirect, if one is set.
193 if ($form_state['redirect']) {
194 if (is_array($form_state['redirect'])) {
195 call_user_func_array('drupal_goto', $form_state['redirect']);
196 }
197 else {
198 drupal_goto($form_state['redirect']);
199 }
200 }
201 }
202 else if (isset($form_state['ajax next'])) {
203 // Clear a few items off the form state so we don't double post:
204 $next = $form_state['ajax next'];
205 unset($form_state['ajax next']);
206 unset($form_state['executed']);
207 unset($form_state['post']);
208 unset($form_state['next']);
209 return ctools_wizard_multistep_form($form_info, $next, $form_state);
210 }
211
212 // If the callbacks wanted to do something besides go to the next form,
213 // it needs to have set $form_state['commands'] with something that can
214 // be rendered.
215 }
216
217 // Render ajax commands if we have any.
218 if (isset($form_state['ajax']) && isset($form_state['commands']) && empty($form_state['modal return'])) {
219 return ajax_render($form_state['commands']);
220 }
221
222 // Otherwise, return the output.
223 return $output;
224 }
225
226 /**
227 * Provide a wrapper around another form for adding multi-step information.
228 */
229 function ctools_wizard_wrapper($form, &$form_state) {
230 $form_info = &$form_state['form_info'];
231 $info = $form_info['forms'][$form_state['step']];
232
233 // Determine the next form from this step.
234 // Create a form trail if we're supposed to have one.
235 $trail = array();
236 $previous = TRUE;
237 foreach ($form_info['order'] as $id => $title) {
238 if ($id == $form_state['step']) {
239 $previous = FALSE;
240 $class = 'wizard-trail-current';
241 }
242 elseif ($previous) {
243 $not_first = TRUE;
244 $class = 'wizard-trail-previous';
245 $form_state['previous'] = $id;
246 }
247 else {
248 $class = 'wizard-trail-next';
249 if (!isset($form_state['next'])) {
250 $form_state['next'] = $id;
251 }
252 if (empty($form_info['show trail'])) {
253 break;
254 }
255 }
256
257 if (!empty($form_info['show trail'])) {
258 if (!empty($form_info['free trail'])) {
259 // ctools_wizard_get_path() returns results suitable for
260 // $form_state['redirect] which can only be directly used in
261 // drupal_goto. We have to futz a bit with it.
262 $path = ctools_wizard_get_path($form_info, $id);
263 $options = array();
264 if (!empty($path[1])) {
265 $options = $path[1];
266 }
267 $title = l($title, $path[0], $options);
268 }
269 $trail[] = '<span class="' . $class . '">' . $title . '</span>';
270 }
271 }
272
273 // Display the trail if instructed to do so.
274 if (!empty($form_info['show trail'])) {
275 ctools_add_css('wizard');
276 $form['ctools_trail'] = array(
277 '#markup' => theme(array('ctools_wizard_trail__' . $form_info['id'], 'ctools_wizard_trail'), array('trail' => $trail)),
278 '#weight' => -1000,
279 );
280 }
281
282 if (empty($form_info['no buttons'])) {
283 // Ensure buttons stay on the bottom.
284 $form['buttons'] = array(
285 '#type' => 'actions',
286 '#weight' => 1000,
287 );
288
289 $button_attributes = array();
290 if (!empty($form_state['ajax']) && empty($form_state['modal'])) {
291 $button_attributes = array('class' => array('ctools-use-ajax'));
292 }
293
294 if (!empty($form_info['show back']) && isset($form_state['previous'])) {
295 $form['buttons']['previous'] = array(
296 '#type' => 'submit',
297 '#value' => $form_info['back text'],
298 '#next' => $form_state['previous'],
299 '#wizard type' => 'next',
300 '#weight' => -2000,
301 '#limit_validation_errors' => array(),
302 // hardcode the submit so that it doesn't try to save data.
303 '#submit' => array('ctools_wizard_submit'),
304 '#attributes' => $button_attributes,
305 );
306
307 if (isset($form_info['no back validate']) || isset($info['no back validate'])) {
308 $form['buttons']['previous']['#validate'] = array();
309 }
310 }
311
312 // If there is a next form, place the next button.
313 if (isset($form_state['next']) || !empty($form_info['free trail'])) {
314 $form['buttons']['next'] = array(
315 '#type' => 'submit',
316 '#value' => $form_info['next text'],
317 '#next' => !empty($form_info['free trail']) ? $form_state['step'] : $form_state['next'],
318 '#wizard type' => 'next',
319 '#weight' => -1000,
320 '#attributes' => $button_attributes,
321 );
322 }
323
324 // There are two ways the return button can appear. If this is not the
325 // end of the form list (i.e, there is a next) then it's "update and return"
326 // to be clear. If this is the end of the path and there is no next, we
327 // call it 'Finish'.
328
329 // Even if there is no direct return path (some forms may not want you
330 // leaving in the middle) the final button is always a Finish and it does
331 // whatever the return action is.
332 if (!empty($form_info['show return']) && !empty($form_state['next'])) {
333 $form['buttons']['return'] = array(
334 '#type' => 'submit',
335 '#value' => $form_info['return text'],
336 '#wizard type' => 'return',
337 '#attributes' => $button_attributes,
338 );
339 }
340 else if (empty($form_state['next']) || !empty($form_info['free trail'])) {
341 $form['buttons']['return'] = array(
342 '#type' => 'submit',
343 '#value' => $form_info['finish text'],
344 '#wizard type' => 'finish',
345 '#attributes' => $button_attributes,
346 );
347 }
348
349 // If we are allowed to cancel, place a cancel button.
350 if ((isset($form_info['cancel path']) && !isset($form_info['show cancel'])) || !empty($form_info['show cancel'])) {
351 $form['buttons']['cancel'] = array(
352 '#type' => 'submit',
353 '#value' => $form_info['cancel text'],
354 '#wizard type' => 'cancel',
355 // hardcode the submit so that it doesn't try to save data.
356 '#limit_validation_errors' => array(),
357 '#submit' => array('ctools_wizard_submit'),
358 '#attributes' => $button_attributes,
359 );
360 }
361
362 // Set up optional validate handlers.
363 $form['#validate'] = array();
364 if (function_exists($info['form id'] . '_validate')) {
365 $form['#validate'][] = $info['form id'] . '_validate';
366 }
367 if (isset($info['validate']) && function_exists($info['validate'])) {
368 $form['#validate'][] = $info['validate'];
369 }
370
371 // Set up our submit handler after theirs. Since putting something here will
372 // skip Drupal's autodetect, we autodetect for it.
373
374 // We make sure ours is after theirs so that they get to change #next if
375 // the want to.
376 $form['#submit'] = array();
377 if (function_exists($info['form id'] . '_submit')) {
378 $form['#submit'][] = $info['form id'] . '_submit';
379 }
380 if (isset($info['submit']) && function_exists($info['submit'])) {
381 $form['#submit'][] = $info['submit'];
382 }
383 $form['#submit'][] = 'ctools_wizard_submit';
384 }
385
386 if (!empty($form_state['ajax'])) {
387 $params = ctools_wizard_get_path($form_state['form_info'], $form_state['step']);
388 if (count($params) > 1) {
389 $url = array_shift($params);
390 $options = array();
391
392 $keys = array(0 => 'query', 1 => 'fragment');
393 foreach ($params as $key => $value) {
394 if (isset($keys[$key]) && isset($value)) {
395 $options[$keys[$key]] = $value;
396 }
397 }
398
399 $params = array($url, $options);
400 }
401 $form['#action'] = call_user_func_array('url', $params);
402 }
403
404 if (isset($info['wrapper']) && function_exists($info['wrapper'])) {
405 $form = $info['wrapper']($form, $form_state);
406 }
407
408 if (isset($form_info['wrapper']) && function_exists($form_info['wrapper'])) {
409 $form = $form_info['wrapper']($form, $form_state);
410 }
411 return $form;
412 }
413
414 /**
415 * On a submit, go to the next form.
416 */
417 function ctools_wizard_submit(&$form, &$form_state) {
418 if (isset($form_state['clicked_button']['#wizard type'])) {
419 $type = $form_state['clicked_button']['#wizard type'];
420
421 // if AJAX enabled, we proceed slightly differently here.
422 if (!empty($form_state['ajax'])) {
423 if ($type == 'next') {
424 $form_state['ajax next'] = $form_state['clicked_button']['#next'];
425 }
426 }
427 else {
428 if ($type == 'cancel' && isset($form_state['form_info']['cancel path'])) {
429 $form_state['redirect'] = $form_state['form_info']['cancel path'];
430 }
431 else if ($type == 'next') {
432 $form_state['redirect'] = ctools_wizard_get_path($form_state['form_info'], $form_state['clicked_button']['#next']);
433 if (!empty($_GET['destination'])) {
434 // We don't want drupal_goto redirect this request
435 // back. ctools_wizard_get_path ensures that the destination is
436 // carried over on subsequent pages.
437 unset($_GET['destination']);
438 }
439 }
440 else if (isset($form_state['form_info']['return path'])) {
441 $form_state['redirect'] = $form_state['form_info']['return path'];
442 }
443 else if ($type == 'finish' && isset($form_state['form_info']['cancel path'])) {
444 $form_state['redirect'] = $form_state['form_info']['cancel path'];
445 }
446 }
447 }
448 }
449
450 /**
451 * Create a path from the form info and a given step.
452 */
453 function ctools_wizard_get_path($form_info, $step) {
454 if (is_array($form_info['path'])) {
455 foreach ($form_info['path'] as $id => $part) {
456 $form_info['path'][$id] = str_replace('%step', $step, $form_info['path'][$id]);
457 }
458 $path = $form_info['path'];
459 }
460 else {
461 $path = array(str_replace('%step', $step, $form_info['path']));
462 }
463
464 // If destination is set, carry it over so it'll take effect when
465 // saving. The submit handler will unset destination to avoid drupal_goto
466 // redirecting us.
467 if (!empty($_GET['destination'])) {
468 // Ensure that options is an array.
469 if (!isset($path[1]) || !is_array($path[1])) {
470 $path[1] = array();
471 }
472 // Ensure that the query part of options is an array.
473 $path[1] += array('query' => array());
474 // Add the destination parameter, if not set already.
475 $path[1]['query'] += drupal_get_destination();
476 }
477
478 return $path;
479 }
480
481 /**
482 * Set default parameters and callbacks if none are given.
483 * Callbacks follows pattern:
484 * $form_info['id']_$hook
485 * $form_info['id']_$form_info['forms'][$step_key]_$hook
486 */
487 function ctools_wizard_defaults(&$form_info) {
488 $hook = $form_info['id'];
489 $defaults = array(
490 'show trail' => FALSE,
491 'free trail' => FALSE,
492 'show back' => FALSE,
493 'show cancel' => FALSE,
494 'show return' => FALSE,
495 'next text' => t('Continue'),
496 'back text' => t('Back'),
497 'return text' => t('Update and return'),
498 'finish text' => t('Finish'),
499 'cancel text' => t('Cancel'),
500 );
501
502 if (!empty($form_info['free trail'])) {
503 $defaults['next text'] = t('Update');
504 $defaults['finish text'] = t('Save');
505 }
506
507 $form_info = $form_info + $defaults;
508 // set form callbacks if they aren't defined
509 foreach ($form_info['forms'] as $step => $params) {
510 if (!$params['form id']) {
511 $form_callback = $hook . '_' . $step . '_form';
512 $form_info['forms'][$step]['form id'] = $form_callback;
513 }
514 }
515
516 // set button callbacks
517 $callbacks = array(
518 'back callback' => '_back',
519 'next callback' => '_next',
520 'return callback' => '_return',
521 'cancel callback' => '_cancel',
522 'finish callback' => '_finish',
523 );
524
525 foreach ($callbacks as $key => $callback) {
526 // never overwrite if explicity defined
527 if (empty($form_info[$key])) {
528 $wizard_callback = $hook . $callback;
529 if (function_exists($wizard_callback)) {
530 $form_info[$key] = $wizard_callback;
531 }
532 }
533 }
534 }