Mercurial > hg > rr-repo
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 } |