Projet

Général

Profil

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

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

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
    elseif (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
    // Even if there is no direct return path (some forms may not want you
329
    // leaving in the middle) the final button is always a Finish and it does
330
    // whatever the return action is.
331
    if (!empty($form_info['show return']) && !empty($form_state['next'])) {
332
      $form['buttons']['return'] = array(
333
        '#type' => 'submit',
334
        '#value' => $form_info['return text'],
335
        '#wizard type' => 'return',
336
        '#attributes' => $button_attributes,
337
      );
338
    }
339
    elseif (empty($form_state['next']) || !empty($form_info['free trail'])) {
340
      $form['buttons']['return'] = array(
341
        '#type' => 'submit',
342
        '#value' => $form_info['finish text'],
343
        '#wizard type' => 'finish',
344
        '#attributes' => $button_attributes,
345
      );
346
    }
347

    
348
    // If we are allowed to cancel, place a cancel button.
349
    if ((isset($form_info['cancel path']) && !isset($form_info['show cancel'])) || !empty($form_info['show cancel'])) {
350
      $form['buttons']['cancel'] = array(
351
        '#type' => 'submit',
352
        '#value' => $form_info['cancel text'],
353
        '#wizard type' => 'cancel',
354
        // Hardcode the submit so that it doesn't try to save data.
355
        '#limit_validation_errors' => array(),
356
        '#submit' => array('ctools_wizard_submit'),
357
        '#attributes' => $button_attributes,
358
      );
359
    }
360

    
361
    // Set up optional validate handlers.
362
    $form['#validate'] = array();
363
    if (function_exists($info['form id'] . '_validate')) {
364
      $form['#validate'][] = $info['form id'] . '_validate';
365
    }
366
    if (isset($info['validate']) && function_exists($info['validate'])) {
367
      $form['#validate'][] = $info['validate'];
368
    }
369

    
370
    // Set up our submit handler after theirs. Since putting something here will
371
    // skip Drupal's autodetect, we autodetect for it.
372
    // We make sure ours is after theirs so that they get to change #next if
373
    // the want to.
374
    $form['#submit'] = array();
375
    if (function_exists($info['form id'] . '_submit')) {
376
      $form['#submit'][] = $info['form id'] . '_submit';
377
    }
378
    if (isset($info['submit']) && function_exists($info['submit'])) {
379
      $form['#submit'][] = $info['submit'];
380
    }
381
    $form['#submit'][] = 'ctools_wizard_submit';
382
  }
383

    
384
  if (!empty($form_state['ajax'])) {
385
    $params = ctools_wizard_get_path($form_state['form_info'], $form_state['step']);
386
    if (count($params) > 1) {
387
      $url = array_shift($params);
388
      $options = array();
389

    
390
      $keys = array(0 => 'query', 1 => 'fragment');
391
      foreach ($params as $key => $value) {
392
        if (isset($keys[$key]) && isset($value)) {
393
          $options[$keys[$key]] = $value;
394
        }
395
      }
396

    
397
      $params = array($url, $options);
398
    }
399
    $form['#action'] = call_user_func_array('url', $params);
400
  }
401

    
402
  if (isset($info['wrapper']) && function_exists($info['wrapper'])) {
403
    $form = $info['wrapper']($form, $form_state);
404
  }
405

    
406
  if (isset($form_info['wrapper']) && function_exists($form_info['wrapper'])) {
407
    $form = $form_info['wrapper']($form, $form_state);
408
  }
409
  return $form;
410
}
411

    
412
/**
413
 * On a submit, go to the next form.
414
 */
415
function ctools_wizard_submit(&$form, &$form_state) {
416
  if (isset($form_state['clicked_button']['#wizard type'])) {
417
    $type = $form_state['clicked_button']['#wizard type'];
418

    
419
    // If AJAX enabled, we proceed slightly differently here.
420
    if (!empty($form_state['ajax'])) {
421
      if ($type == 'next') {
422
        $form_state['ajax next'] = $form_state['clicked_button']['#next'];
423
      }
424
    }
425
    else {
426
      if ($type == 'cancel' && isset($form_state['form_info']['cancel path'])) {
427
        $form_state['redirect'] = $form_state['form_info']['cancel path'];
428
      }
429
      elseif ($type == 'next') {
430
        $form_state['redirect'] = ctools_wizard_get_path($form_state['form_info'], $form_state['clicked_button']['#next']);
431
        if (!empty($_GET['destination'])) {
432
          // We don't want drupal_goto redirect this request
433
          // back. ctools_wizard_get_path ensures that the destination is
434
          // carried over on subsequent pages.
435
          unset($_GET['destination']);
436
        }
437
      }
438
      elseif (isset($form_state['form_info']['return path'])) {
439
        $form_state['redirect'] = $form_state['form_info']['return path'];
440
      }
441
      elseif ($type == 'finish' && isset($form_state['form_info']['cancel path'])) {
442
        $form_state['redirect'] = $form_state['form_info']['cancel path'];
443
      }
444
    }
445
  }
446
}
447

    
448
/**
449
 * Create a path from the form info and a given step.
450
 */
451
function ctools_wizard_get_path($form_info, $step) {
452
  if (is_array($form_info['path'])) {
453
    foreach ($form_info['path'] as $id => $part) {
454
      $form_info['path'][$id] = str_replace('%step', $step, $form_info['path'][$id]);
455
    }
456
    $path = $form_info['path'];
457
  }
458
  else {
459
    $path = array(str_replace('%step', $step, $form_info['path']));
460
  }
461

    
462
  // If destination is set, carry it over so it'll take effect when
463
  // saving. The submit handler will unset destination to avoid drupal_goto
464
  // redirecting us.
465
  if (!empty($_GET['destination'])) {
466
    // Ensure that options is an array.
467
    if (!isset($path[1]) || !is_array($path[1])) {
468
      $path[1] = array();
469
    }
470
    // Add the destination parameter, if not set already.
471
    $path[1] += drupal_get_destination();
472
  }
473

    
474
  return $path;
475
}
476

    
477
/**
478
 * Set default parameters and callbacks if none are given.
479
 * Callbacks follows pattern:
480
 * $form_info['id']_$hook
481
 * $form_info['id']_$form_info['forms'][$step_key]_$hook
482
 */
483
function ctools_wizard_defaults(&$form_info) {
484
  $hook = $form_info['id'];
485
  $defaults = array(
486
    'show trail' => FALSE,
487
    'free trail' => FALSE,
488
    'show back' => FALSE,
489
    'show cancel' => FALSE,
490
    'show return' => FALSE,
491
    'next text' => t('Continue'),
492
    'back text' => t('Back'),
493
    'return text' => t('Update and return'),
494
    'finish text' => t('Finish'),
495
    'cancel text' => t('Cancel'),
496
  );
497

    
498
  if (!empty($form_info['free trail'])) {
499
    $defaults['next text'] = t('Update');
500
    $defaults['finish text'] = t('Save');
501
  }
502

    
503
  $form_info = $form_info + $defaults;
504
  // Set form callbacks if they aren't defined.
505
  foreach ($form_info['forms'] as $step => $params) {
506
    if (empty($params['form id'])) {
507
      $form_callback = $hook . '_' . $step . '_form';
508
      $form_info['forms'][$step]['form id'] = $form_callback;
509
    }
510
  }
511

    
512
  // Set button callbacks.
513
  $callbacks = array(
514
    'back callback' => '_back',
515
    'next callback' => '_next',
516
    'return callback' => '_return',
517
    'cancel callback' => '_cancel',
518
    'finish callback' => '_finish',
519
  );
520

    
521
  foreach ($callbacks as $key => $callback) {
522
    // Never overwrite if explicity defined.
523
    if (empty($form_info[$key])) {
524
      $wizard_callback = $hook . $callback;
525
      if (function_exists($wizard_callback)) {
526
        $form_info[$key] = $wizard_callback;
527
      }
528
    }
529
  }
530
}