diff --git a/css/wizard.css b/css/wizard.css new file mode 100644 index 0000000000000000000000000000000000000000..80adc7ff6a27c8577ca243da7394b5b017231f56 --- /dev/null +++ b/css/wizard.css @@ -0,0 +1,9 @@ +/* $Id$ */ + +.wizard-trail { + font-size: 120%; +} + +.wizard-trail-current { + font-weight: bold; +} diff --git a/delegator/delegator.admin.inc b/delegator/delegator.admin.inc index 473f6a1d002903788310303cdad970be5f4af363..37546c71b33be6b5ffb57d996e7cb315c8a96fcb 100644 --- a/delegator/delegator.admin.inc +++ b/delegator/delegator.admin.inc @@ -1091,7 +1091,7 @@ function delegator_admin_edit_task_handler(&$form_state) { $form['buttons'] = array( '#prefix' => '<div class="clear-block">', '#suffix' => '</div>', - '#weight' => 1000 + '#weight' => 1000, ); if (isset($form_state['next'])) { @@ -1101,6 +1101,7 @@ function delegator_admin_edit_task_handler(&$form_state) { '#next' => $form_state['next'], '#validate' => $validate, '#submit' => $submit, + '#weight' => -1000, ); } @@ -1296,7 +1297,7 @@ function delegator_admin_import_task_handler_validate($form, &$form_state) { $handler->export_type = EXPORT_IN_DATABASE; if (isset($cache->handlers[$handler->name])) { - drupal_set_message(t('Warning: The handler you are important already exists and is overwriting an existing handler. If this is not what you intend, you may Cancel this. You should then modify the <code>$handler->name</code> field of your import to have a unique name.'), 'warning'); + drupal_set_message(t('Warning: The handler you are importing already exists and this operation will overwrite an existing handler. If this is not what you intend, you may Cancel this. You should then modify the <code>$handler->name</code> field of your import to have a unique name.'), 'warning'); $old_handler = delegator_admin_find_handler($handler->name, $cache); $handler->export_type = $old_handler->export_type | EXPORT_IN_DATABASE; diff --git a/delegator/delegator.install b/delegator/delegator.install index 4b0ba30c34b7b0e9b276af3dd36d9e3862cc4c2c..9d031b9ee2209b3178fd674035a0a9ad30678c5d 100644 --- a/delegator/delegator.install +++ b/delegator/delegator.install @@ -87,6 +87,70 @@ function delegator_schema_1() { 'weights' => array('name', 'weight'), ), ); + + $schema['delegator_pages'] = array( + 'description' => t('Contains page subtasks for implementing pages with arbitrary tasks.'), + 'fields' => array( + 'pid' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => t('Primary ID field for the table. Not used for anything except internal lookups.'), + 'no export' => TRUE, + ), + 'name' => array( + 'type' => 'varchar', + 'length' => '255', + 'description' => t('Unique ID for this subtask. Used to identify it programmatically.'), + ), + 'admin_title' => array( + 'type' => 'varchar', + 'length' => '255', + 'description' => t('Human readable title for this page subtask.'), + ), + 'path' => array( + 'type' => 'varchar', + 'length' => '255', + 'description' => t('The menu path that will invoke this task.'), + ), + 'access' => array( + 'type' => 'text', + 'size' => 'big', + 'description' => t('Access configuration for this path.'), + 'not null' => TRUE, + 'default' => '', + 'serialize' => TRUE, + ), + 'menu' => array( + 'type' => 'text', + 'size' => 'big', + 'description' => t('Serialized configuration of Drupal menu visibility settings for this item.'), + 'not null' => TRUE, + 'default' => '', + 'serialize' => TRUE, + ), + 'parent_menu' => array( + 'type' => 'text', + 'size' => 'big', + 'description' => t('Serialized configuration of Drupal parent menu visibility settings for this item.'), + 'not null' => TRUE, + 'default' => '', + 'serialize' => TRUE, + ), + 'arguments' => array( + 'type' => 'text', + 'size' => 'big', + 'description' => t('Configuration of arguments for this menu item.'), + 'not null' => TRUE, + 'default' => '', + 'serialize' => TRUE, + ), + ), + 'primary key' => array('pid'), + 'unique keys' => array( + 'name' => array('name'), + ), + ); + return $schema; } diff --git a/delegator/delegator.module b/delegator/delegator.module index eb215f90a5fa6206875598cd9df3a6137e1e328e..942c7b6c014ddcdd3ccf56ed2ac9c3e671114328 100644 --- a/delegator/delegator.module +++ b/delegator/delegator.module @@ -318,6 +318,10 @@ function delegator_delete_task_handler($handler) { db_query("DELETE FROM {delegator_weights} WHERE name = '%s'", $handler->name); } +/** + * Export a task handler into code suitable for import or use as a default + * task handler. + */ function delegator_export_task_handler($handler, $indent = '') { ctools_include('export'); ctools_include('plugins'); diff --git a/help/wizard.html b/help/wizard.html new file mode 100644 index 0000000000000000000000000000000000000000..ebe7a709e838a374595fce31952509bb5f0f8a88 --- /dev/null +++ b/help/wizard.html @@ -0,0 +1,28 @@ +<!-- $Id$ --> +form_info => array( + 'id' => An id for this multistep. Will be used for things like trail theming. + + 'path' => /path/to/form/%step, + 'return path' => Where to go when exiting the wizard. Required [maybe] + + 'show trail' => (default false), + 'show back' => (default false) -- show a back button + 'show return' => (default false) show a return button + + // callbacks + 'finish callback', + 'cancel callback', + 'return callback', + 'next callback' + + // form info + 'order' => array( + 'id' => t('title'), + ), + 'forms' => array( + 'form_id' => array( + 'include' => .., + 'form id' => .., + ), + ), + diff --git a/includes/export.inc b/includes/export.inc index 938014886c058f9ce2f0811f4f0192030fa2c589..f2f7b22b7455462158d1c29e397a6f5f57cc96e3 100644 --- a/includes/export.inc +++ b/includes/export.inc @@ -416,4 +416,35 @@ function ctools_export_form(&$form_state, $code, $title = '') { ); return $form; -} \ No newline at end of file +} + +/** + * Create a new object based upon schema values. + * + * Because 'default' has ambiguous meaning on some fields, we will actually + * use 'object default' to fill in default values if default is not set + * That's a little safer to use as it won't cause weird database default situations. + */ +function ctools_export_new_object($table) { + $schema = ctools_export_get_schema($table); + $export = $schema['export']; + + $object = new stdClass; + foreach ($schema['fields'] as $field => $info) { + if (isset($info['object default'])) { + $object->$field = $info['object default']; + } + else if (isset($info['default'])) { + $object->$field = $info['default']; + } + else { + $object->$field = NULL; + } + } + + // Set some defaults so this data always exists. + + $object->export_type = EXPORT_IN_DATABASE; + $object->type = t('Local'); + return $object; +} diff --git a/includes/form.inc b/includes/form.inc index 48be9f141d1650b00e472c19fac3cbf26167e4d2..4ac55576b140bda0090ec2e324e71f8d476c5d9d 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -53,15 +53,32 @@ function ctools_build_form($form_id, &$form_state) { // to build it from scratch. if (!isset($form)) { $form_state['post'] = $form_state['input']; - // Use a copy of the function's arguments for manipulation - $args_temp = $args; - $args_temp[0] = &$form_state; - array_unshift($args_temp, $form_id); + // This allows us to do some interesting form embedding stuff without + // messing up the form IDs too badly. + if (isset($form_state['wrapper callback']) && function_exists($form_state['wrapper callback'])) { + // If there is a wrapper callback, we do not use drupal_retrieve_form. + // Instead, we call $form_id builder function directly. This means the args + // are *different* for forms used this way, which may not be ideal but + // is necessary right now. + $form = array(); + $form_state['wrapper callback']($form, $form_state); + if (function_exists($form_id)) { + $form_id($form, $form_state); + } + } + else { + // Use a copy of the function's arguments for manipulation + $args_temp = $args; + $args_temp[0] = &$form_state; + array_unshift($args_temp, $form_id); + + $form = call_user_func_array('drupal_retrieve_form', $args_temp); + } - $form = call_user_func_array('drupal_retrieve_form', $args_temp); $form_build_id = 'form-' . md5(mt_rand()); $form['#build_id'] = $form_build_id; + if ($form_state['method'] == 'get' && !isset($form['#method'])) { $form['#method'] = 'get'; } diff --git a/includes/wizard.inc b/includes/wizard.inc new file mode 100644 index 0000000000000000000000000000000000000000..939ffd3b0bf375c0901e3d3b4f51d336d8d6ed16 --- /dev/null +++ b/includes/wizard.inc @@ -0,0 +1,237 @@ +<?php +// $Id$ + +/** + * @file + * CTools' multi-step form wizard tool. + * + * This tool enables the creation of multi-step forms that go from one + * form to another. The forms themselves can allow branching if they + * like, and there are a number of configurable options to how + * the wizard operates. + * + * The wizard can also be friendly to ajax forms, such as when used + * with the modal tool. + * + * TODO: should the wizard also perform object caching? We'll see + * as we develop this if it should happen within the wizard + * or outside the wizard. + */ + +/** + * Display a multi-step form. + * + * Aside from the addition of the $form_info which contains an array of + * information and configuration so the multi-step wizard can do its thing, + * this function works a lot like ctools_build_form. + * + * Remember that the form builders for this form will receive + * &$form, &$form_state, NOT just &$form_state and no additional args. + * + * Do NOT use #required => 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) { + $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'; + $form_state['re_render'] = FALSE; + $form_state['no_redirect'] = TRUE; + + ctools_include('form'); + $output = ctools_build_form($info['form id'], $form_state); + + if (!$output) { + // 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 the finish button was clicked, call the finish callback. + if ($function = ctools_plugin_get_function($form_info, "$type callback")) { + $function($form_state); + } + } + + // redirect, if one is set. + drupal_redirect_form(array(), $form_state['redirect']); + } + + 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'])) { + $trail[] = '<span class="' . $class . '">' . $title . '</span>'; + } + } + + // Display the trail if instructed to do so. + if (!empty($form_info['show trail'])) { + ctools_add_css('wizard'); +// drupal_add_css(drupal_get_path('module', 'delegator') . '/css/task-handlers.css'); + $form['ctools_trail'] = array( + '#value' => theme(array('ctools_wizard_trail__' . $form_info['id'], 'ctools_wizard_trail'), $trail), + '#weight' => -1000, + ); + } + + // Ensure buttons stay on the bottom. + $form['buttons'] = array( + '#prefix' => '<div class="clear-block">', + '#suffix' => '</div>', + '#weight' => 1000, + ); + + if (!empty($form_info['show back']) && isset($form_state['previous'])) { + $form['buttons']['previous'] = array( + '#type' => 'submit', + '#value' => t('Back'), + '#next' => $form_state['previous'], + '#wizard type' => 'next', + '#weight' => -2000, + // hardcode the submit so that it doesn't try to save data. + '#submit' => array('ctools_wizard_submit'), + ); + } + + // If there is a next form, place the next button. + if (isset($form_state['next'])) { + $form['buttons']['next'] = array( + '#type' => 'submit', + '#value' => t('Continue'), + '#next' => $form_state['next'], + '#wizard type' => 'next', + '#weight' => -1000, + ); + } + + // 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' => t('Update and return'), + '#wizard type' => 'return', + ); + } + else if (empty($form_state['next'])) { + $form['buttons']['return'] = array( + '#type' => 'submit', + '#value' => t('Finish'), + '#wizard type' => 'finish', + ); + } + + // If we are allowed to cancel, place a cancel button. + if (isset($form_info['cancel path'])) { + $form['buttons']['cancel'] = array( + '#type' => 'submit', + '#value' => t('Cancel'), + '#submit' => array('ctools_wizard_cancel'), + '#wizard type' => 'cancel', + ); + } + + // 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'; + } + $form['#submit'][] = 'ctools_wizard_submit'; +} + +/** + * 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 ($type == 'return' || $type == 'finish') { + $form_state['redirect'] = $form_state['form_info']['return path']; + // Do we need to do something here or just let it go? + } + else { + $form_state['redirect'] = ctools_wizard_get_path($form_state['form_info'], $form_state['clicked_button']['#next']); + } + } +} + +/** + * Create a path from the form info and a given step. + */ +function ctools_wizard_get_path($form_info, $step) { + return str_replace('%step', $step, $form_info['path']); +} diff --git a/includes/wizard.theme.inc b/includes/wizard.theme.inc new file mode 100644 index 0000000000000000000000000000000000000000..643b05ef4513d68d8632fab8bf8ce89eac0b20c2 --- /dev/null +++ b/includes/wizard.theme.inc @@ -0,0 +1,24 @@ +<?php +// $Id$ +/** + * @file + * Themable for the wizard tool. + */ + +function ctools_wizard_theme(&$theme) { + $theme['ctools_wizard_trail'] = array( + 'arguments' => array('trail'), + 'file' => 'includes/wizard.theme.inc', + ); +} + +/** + * Themable display of the 'breadcrumb' trail to show the order of the + * forms. + */ +function theme_ctools_wizard_trail($trail) { + if (!empty($trail)) { + return '<div class="wizard-trail">' . implode(' » ', $trail) . '</div>'; + } +} +