Projet

Général

Profil

Paste
Télécharger (17,7 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / ctools / includes / wizard.inc @ e4c061ad

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, 'form_info' => $form_info)),
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
}