diff --git css/export-ui-list.css css/export-ui-list.css new file mode 100644 index 0000000..3a778ab --- /dev/null +++ css/export-ui-list.css @@ -0,0 +1,28 @@ +/* $Id: panels-item.css,v 1.1.2.1 2010/02/17 01:09:46 merlinofchaos Exp $ */ +body form#ctools-export-ui-list-form { + margin: 0 0 20px 0; +} + +#ctools-export-ui-list-form .form-item { + padding-right: 1em; /* LTR */ + float: left; /* LTR */ + margin-top: 0; + margin-bottom: 0; +} + +#ctools-export-ui-list-items { + width: 100%; +} + +#edit-order-wrapper { + clear: left; /* LTR */ +} + +#ctools-export-ui-list-form .form-submit { + margin-top: 1.65em; + float: left; /* LTR */ +} + +tr.ctools-export-ui-disabled { + color: #999; +} diff --git ctools.module ctools.module index ad3cb4e..37d45b7 100644 --- ctools.module +++ ctools.module @@ -284,7 +284,7 @@ function _ctools_passthrough(&$items, $type = 'theme') { require_once './' . $file->filename; list($tool) = explode('.', $file->name, 2); - $function = 'ctools_' . $tool . '_' . $type; + $function = 'ctools_' . str_replace ('-', '_', $tool) . '_' . $type; if (function_exists($function)) { $function($items); } @@ -760,3 +760,46 @@ function ctools_file_check_directory(&$directory, $mode = 0, $form_item = NULL) return TRUE; } + +/** + * Menu loader; Load exportables when used with export-ui. + */ +function ctools_export_ui_load($item_name, $plugin_name) { + $return = &ctools_static(__FUNCTION__, FALSE); + + if (!$return) { + ctools_include('export'); + ctools_include('export-ui'); + $plugin = ctools_get_export_ui($plugin_name); + + if ($plugin) { + // Get the load callback. + $schema = ctools_export_get_schema($plugin['schema']); + $return = call_user_func($schema['export']['load callback'], $item_name); + } + } + + return $return; +} + +/** + * Menu access callback for various tasks of export-ui. + */ +function ctools_export_ui_task_access($export, $op) { + // TODO: This needs to be more configurable. + + if (!user_access('administer site configuration')) { + return FALSE; + } + switch ($op) { + case 'revert': + return ($export->export_type & EXPORT_IN_DATABASE) && ($export->export_type & EXPORT_IN_CODE); + case 'delete': + return ($export->export_type & EXPORT_IN_DATABASE) && !($export->export_type & EXPORT_IN_CODE); + case 'disable': + return empty($export->disabled); + case 'enable': + return !empty($export->disabled); + } + return TRUE; +} diff --git includes/export-ui.admin.inc includes/export-ui.admin.inc new file mode 100644 index 0000000..ad77dbe --- /dev/null +++ includes/export-ui.admin.inc @@ -0,0 +1,213 @@ +list_page($js, $_POST); + } + else { + return t('Configuration error. No handler found.'); + } +} + +/** + * Main page callback to manipulate exportables. + * + * This simply loads the object defined in the plugin and hands it off to + * a method based upon the name of the operation in use. This can easily + * be used to add more ops. + */ +function ctools_export_ui_switcher_page($plugin_name, $op, $item = array()) { + $js = !empty($_REQUEST['ctools_ajax']); + + // Load the $plugin information + ctools_include('export'); + $plugin = ctools_get_export_ui($plugin_name); + + $handler = ctools_export_ui_get_handler($plugin); + if ($handler) { + $method = $op . '_page'; + if (method_exists($handler, $method)) { + $args = array( + 'js' => $js, + 'input' => $_POST, + 'item' => $item, + ); + return call_user_func_array(array($handler, $method), $args); + } + } + else { + return t('Configuration error. No handler found.'); + } +} + +/** + * Provide a form to confirm one of the provided actions. + */ +function ctools_export_ui_confirm(&$form_state, $plugin_name, $op = 'delete', $export) { + $plugin = ctools_get_export_ui($plugin_name); + + $form = array(); + $form['export'] = array('#type' => 'value', '#value' => $export); + $form['action'] = array('#type' => 'value', '#value' => $op); + $form['plugin'] = array('#type' => 'value', '#value' => $plugin); + + $export_key = $plugin['export']['key']; + $question = str_replace('!action', $plugin['allowed operations'][$op], $plugin['form']['string']['confirmation']['question']); + $question = str_replace('%title', $export->{$export_key}, $plugin['form']['string']['confirmation']['question']); + + $form = confirm_form($form, + $question, + ctools_export_ui_plugin_base_path($plugin['name']), + $plugin['form']['string']['confirmation'][$op], + drupal_ucfirst($plugin['allowed operations'][$op]), t('Cancel') + ); + return $form; +} + +/** + * Submit handler for the ctools_export_ui_confirm form. + */ +function ctools_export_ui_confirm_submit($form, &$form_state) { + ctools_include('export'); + $plugin = $form['plugin']['#value']; + + $export = $form_state['values']['export']; + switch ($form_state['values']['action']) { + case 'revert': + case 'delete': + $schema = ctools_export_get_schema($plugin['schema']); + call_user_func($schema['export']['delete callback'], $export); + break; + } + $form_state['redirect'] = ctools_export_ui_plugin_base_path($plugin['name']); +} + +/** + * Enable or disable an exportable. + */ +function ctools_export_ui_switcher(&$form_state, $plugin_name, $op = 'enable', $export) { + $plugin = ctools_get_export_ui($plugin_name); + $export_key = $plugin['export']['key']; + + ctools_export_set_object_status($export, $op != 'enable'); + drupal_set_message(str_replace('%title', $export->{$export_key}, $plugin['form']['string']['message'][$op])); + drupal_goto(ctools_export_ui_plugin_base_path($plugin['name'])); +} + +/** + * Page callback for import form. Switches form output to export form + * if import submission has occurred. + */ +function ctools_export_ui_import_page($plugin_name) { + if (!empty($_POST) && $_POST['form_id'] == 'ctools_export_ui_form') { + return drupal_get_form('ctools_export_ui_form', $plugin_name, 'add'); + } + return drupal_get_form('ctools_export_ui_import', $plugin_name); +} + +/** + * Import form. Provides simple helptext instructions and textarea for + * pasting a export definition. + */ +function ctools_export_ui_import($form_state, $plugin_name) { + ctools_include('export-ui'); + $plugin = ctools_get_export_ui($plugin_name); + + drupal_set_title($plugin['form']['string']['title']['import']); + + $form = array(); + $form['plugin'] = array( + '#type' => 'value', + '#value' => $plugin, + ); + + $form['help'] = array( + '#type' => 'item', + '#value' => $plugin['form']['string']['help']['import'], + ); + $form['import'] = array( + '#title' => t('@plugin object', array('@plugin' => $plugin['title'])), + '#type' => 'textarea', + '#rows' => 10, + '#required' => TRUE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Import'), + ); + return $form; +} + +/** + * Import form submit handler. Evaluates import code and transfers to + * export definition form. + */ +function ctools_export_ui_import_submit($form, &$form_state) { + $plugin = $form['plugin']['#value']; + $export_key = $plugin['export']['key']; + + $items = array(); + if ($import = $form_state['values']['import']) { + ob_start(); + $export = eval($import); + ob_end_clean(); + } + + if (is_object($export)) { + if (!empty($export->{$export_key})) { + + $schema = ctools_export_get_schema($plugin['schema']); + + // TODO: If $schema['export']['load callback'] == FALSE we should issue + // an error - but maybe it should happen in a more generic place. + if (call_user_func($schema['export']['load callback'], $export->{$export_key})) { + drupal_set_message(t('A @plugin with this name already exists. Please remove the existing export before importing this definition.', array('@plugin' => $plugin['title'])), 'error'); + } + else { + drupal_set_title($plugin['form']['string']['title']['export']); + $output = drupal_get_form('ctools_export_ui_form', $plugin['name'], 'add', (object) $export); + print theme('page', $output); + exit; + } + } + } + else { + drupal_set_message(t('An error occurred while importing. Please check your export definition.', 'error')); + + $form_state['redirect'] = ctools_export_ui_plugin_base_path($plugin['name']); + } +} + +/** + * Provides a form with an exported export definition for use in modules. + * + * @param $cid + * A export id. + * + * @return + * A FormAPI array. + */ +function ctools_export_ui_export(&$form_state, $export, $plugin_name) { + ctools_include('export-ui'); + $plugin = ctools_get_export_ui($plugin_name); + + $export_key = $plugin['export']['key']; + + drupal_set_title(str_replace('%title', $plugin['title'], $plugin['form']['string']['title']['export'])); + + return ctools_export_form($form_state, ctools_export_ui_export_object($plugin, $export), t('Export')); +} diff --git includes/export-ui.inc includes/export-ui.inc new file mode 100644 index 0000000..32eeed0 --- /dev/null +++ includes/export-ui.inc @@ -0,0 +1,344 @@ + 'ctools_export_ui_defaults', + ); +} + +/** + * Provide defaults for an export-ui plugin. + */ +function ctools_export_ui_defaults($info, &$plugin) { + ctools_include('export'); + + $plugin += array( + 'has menu' => TRUE, + 'title' => $plugin['name'], + 'export' => array(), + 'allowed operations' => array(), + 'menu' => array(), + 'form' => array(), + 'list' => NULL, + ); + + if (empty($plugin['schema'])) { + if ($plugin['has menu']) { + // We need to issue a warning as schema is a required key. + drupal_set_message(t('The plugin definition of @plugin is missing the "schema" key.', array('@plugin' => $plugin['name'])), 'error'); + } + } + else { + $schema = ctools_export_get_schema($plugin['schema']); + + $plugin['export'] += array( + // Add the identifier key from the schema so we don't have to call + // ctools_export_get_schema() just for that. + 'key' => $schema['export']['key'], + ); + + // Add some default fields that appear often in exports + // If these use different keys they can easily be specified in the + // $plugin. + + if (empty($plugin['export']['admin_title']) && !empty($schema['fields']['admin_title'])) { + $plugin['export']['admin_title'] = 'admin_title'; + } + if (empty($plugin['export']['admin_description']) && !empty($schema['fields']['admin_description'])) { + $plugin['export']['admin_description'] = 'admin_description'; + } + } + + // Define allowed operations, and the name of the operations. + $plugin['allowed operations'] += array( + 'edit' => t('Edit'), + 'enable' => t('Enable'), + 'disable' => t('Disable'), + 'revert' => t('Revert'), + 'delete' => t('Delete'), + 'clone' => t('Clone'), + 'import' => t('Import'), + 'export' => t('Export'), + ); + + if ($plugin['has menu']) { + $plugin['menu'] += array( + 'menu item' => str_replace(' ', '-', $plugin['name']), + 'menu prefix' => 'admin/build', + ); + + $prefix_count = count(explode('/', $plugin['menu']['menu prefix'])); + + $plugin['menu'] += array( + // Default menu items that should be declared. + 'items' => array( + 'list callback' => array( + 'path' => '', + // Menu items are translated by the menu system. + // TODO: We need more flexibility in title. The title of the admin page + // is not necessarily the title of the object, plus we need + // plural, singular, proper, not proper, etc. + 'title' => $plugin['title'], + 'description' => 'List '. $plugin['title'], + 'page callback' => 'ctools_export_ui_list_items', + 'page arguments' => array($plugin['name']), + 'type' => MENU_NORMAL_ITEM, + ), + 'list' => array( + 'path' => 'list', + 'title' => 'List', + 'description' => 'List '. $plugin['title'], + 'page callback' => 'ctools_export_ui_list_items', + 'page arguments' => array( $plugin['name']), + 'type' => MENU_DEFAULT_LOCAL_TASK, + ), + 'add' => array( + 'path' => 'add', + 'title' => 'Add', + 'description' => 'Add a new '. $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'add'), + 'type' => MENU_LOCAL_TASK, + ), + 'edit callback' => array( + 'path' => 'list/%ctools_export_ui', + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'edit', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'type' => MENU_CALLBACK, + ), + 'edit' => array( + 'path' => 'list/%ctools_export_ui/edit', + 'title' => 'Edit', + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'edit', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'type' => MENU_DEFAULT_LOCAL_TASK, + ), + ), + ); + + if ($plugin['allowed operations']['import']) { + $plugin['menu']['items'] += array( + 'import' => array( + 'path' => 'import', + 'title' => 'Import', + 'description' => 'Import '. $plugin['title'] .' to your site.', + // We allow permissions to import only to users that are allowed to + // execute php. + 'access callback' => 'ctools_access_multiperm', + 'access arguments' => array('use PHP for block visibility'), + 'page callback' => 'ctools_export_ui_import_page', + 'page arguments' => array($plugin['name']), + 'type' => MENU_LOCAL_TASK, + ), + ); + } + + if ($plugin['allowed operations']['export']) { + $plugin['menu']['items'] += array( + 'export' => array( + 'path' => 'list/%ctools_export_ui/export', + 'title' => 'Export', + 'description' => 'Export '. $plugin['title'], + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ctools_export_ui_export', $prefix_count + 2, $plugin['name']), + 'load arguments' => array($plugin['name']), + 'type' => MENU_LOCAL_TASK, + ), + ); + } + + if ($plugin['allowed operations']['revert']) { + $plugin['menu']['items'] += array( + 'revert' => array( + 'path' => 'list/%ctools_export_ui/revert', + 'title' => 'Revert', + 'description' => 'Revert '. $plugin['title'], + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ctools_export_ui_confirm', $plugin['name'], 'revert', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($prefix_count + 2, 'revert'), + 'type' => MENU_CALLBACK, + ), + ); + } + + if ($plugin['allowed operations']['clone']) { + $plugin['menu']['items'] += array( + 'clone' => array( + 'path' => 'list/%ctools_export_ui/clone', + 'title' => 'Clone', + 'description' => 'Revert '. $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'clone', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'type' => MENU_CALLBACK, + ), + ); + } + + if ($plugin['allowed operations']['delete']) { + $plugin['menu']['items'] += array( + 'delete' => array( + 'path' => 'list/%ctools_export_ui/delete', + 'title' => 'Delete', + 'description' => 'Delete '. $plugin['title'], + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ctools_export_ui_confirm', $plugin['name'], 'delete', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($prefix_count + 2, 'delete'), + 'type' => MENU_CALLBACK, + ), + ); + } + + if ($plugin['allowed operations']['enable']) { + $plugin['menu']['items'] += array( + 'enable' => array( + 'path' => 'list/%ctools_export_ui/enable', + 'title' => 'Enable', + 'description' => 'Enable '. $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'enable', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($prefix_count + 2, 'enable'), + 'type' => MENU_CALLBACK, + ), + ); + } + + if ($plugin['allowed operations']['disable']) { + $plugin['menu']['items'] += array( + 'disable' => array( + 'path' => 'list/%ctools_export_ui/disable', + 'title' => 'Disable', + 'description' => 'Disable '. $plugin['title'], + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'disable', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($prefix_count + 2, 'disable'), + 'type' => MENU_CALLBACK, + ), + ); + } + } + + // Define form elements. + $plugin['form'] += array( + 'settings' => function_exists($plugin['name'] . '_form') ? $plugin['name'] . '_form' : '', + 'string' => array( + // Strings used in drupal_set_title(). + 'title' => array( + 'add' => t('Add a new @plugin', array('@plugin' => $plugin['title'])), + // The "%title" will be replaced in ctools_export_ui_form(), as in this + // stage we dont have the specific exportable object. + 'edit' => t('Editing @plugin %title', array('@plugin' => $plugin['title'])), + 'view' => t('Viewing @plugin %title', array('@plugin' => $plugin['title'])), + 'clone' => t('Cloning @plugin %title', array('@plugin' => $plugin['title'])), + + 'import' => t('Import @plugin', array('@plugin' => $plugin['title'])), + 'export' => t('Export @plugin', array('@plugin' => $plugin['title'])), + ), + // Strings used in confirmation pages. + 'confirmation' => array( + // The "!action" and "%title" will be replaced in + // ctools_export_ui_form(). + 'question' => t('Are you sure you want to !action the @plugin %title?', array('@plugin' => $plugin['title'])), + 'revert' => t('This action will permanently remove any customizations made to this export.'), + 'delete' => t('This action will remove this export permanently from your site.'), + ), + // Strings used in $forms. + 'help' => array( + 'import' => t('You can import an exported definition by pasting the exported object code into the field below.'), + ), + // Strings used in drupal_set_message(). + 'message' => array( + 'enable' => t('@plugin %title was enabled.', array('@plugin' => $plugin['title'])), + 'disable' => t('@plugin %title was disabled.', array('@plugin' => $plugin['title'])), + ), + ), + ); +} + +/** + * Get the class to handle creating a list of exportable items. + * + * @return + * Either the lister class or FALSE if one could not be had. + */ +function ctools_export_ui_get_handler($plugin) { + $cache = &ctools_static(__FUNCTION__, array()); + if (empty($cache[$plugin['name']])) { + // If a list class is not specified by the plugin, fall back to the + // default ctools_export_ui plugin instead. + if (empty($plugin['list'])) { + $default = ctools_get_export_ui('ctools_export_ui'); + $class = ctools_plugin_get_class($default, 'handler'); + } + else { + $class = ctools_plugin_get_class($plugin, 'handler'); + } + + if ($class) { + $cache[$plugin['name']] = new $class(); + $cache[$plugin['name']]->init($plugin); + } + } + return !empty($cache[$plugin['name']]) ? $cache[$plugin['name']] : FALSE; +} + +/** + * CTools export function. + */ +function ctools_export_ui_export_object($plugin, $export, $indent = '') { + $schema = ctools_export_get_schema($plugin['schema']); + + ctools_include('export'); + return ctools_export_object($plugin['schema'], $export, $indent); +} + +/** + * Get redirection path from a plugin. + * + * @param $plguin_name + * The plugin name. + * + * @return + * The menu path to the plugin's list. + */ +function ctools_export_ui_plugin_base_path($plugin_name) { + $plugin = ctools_get_export_ui($plugin_name); + + return $plugin['menu']['menu prefix'] .'/'. $plugin['menu']['menu item']; +} + +/** + * Helper function to include CTools plugins and get an export-ui exportable. + * + * @param $plugin_name + * The plugin that should be laoded. + */ +function ctools_get_export_ui($plugin_name) { + ctools_include('plugins'); + return ctools_get_plugins('ctools', 'export_ui', $plugin_name); + +} + +/** + * Helper function to include CTools plugins and get all export-ui exportables. + */ +function ctools_get_export_uis() { + ctools_include('plugins'); + return ctools_get_plugins('ctools', 'export_ui', $plugin_name); +} diff --git includes/export-ui.menu.inc includes/export-ui.menu.inc new file mode 100644 index 0000000..cdea750 --- /dev/null +++ includes/export-ui.menu.inc @@ -0,0 +1,33 @@ + array('administer site configuration'), + 'file' => 'export-ui.admin.inc', + 'file path' => drupal_get_path('module', 'ctools') .'/includes', + // Add the map, so we can get the plugin in export_ui_load(). + 'load arguments' => array('%map'), + ); + + $path = !empty($item['path']) ? $prefix .'/'. $item['path'] : $prefix; + unset($item['path']); + $items[$path] = $item; + } + } + } +} diff --git includes/export.inc includes/export.inc index b465d38..f3d2277 100644 --- includes/export.inc +++ includes/export.inc @@ -460,6 +460,13 @@ function ctools_export_get_schema($table) { 'export callback' => "$schema[module]_export_{$table}", 'list callback' => "$schema[module]_{$table}_list", 'to hook code callback' => "$schema[module]_{$table}_to_hook_code", + + // Define CRUD functions, if none are defined in the exportable + // schema. + 'create callback' => 'ctools_export_new_object', + 'save callback' => function_exists("$schema[module]_save") ? "$schema[module]_save" : FALSE, + 'delete callback' => function_exists("$schema[module]_delete") ? "$schema[module]_delete" : FALSE, + 'load callback' => function_exists("$schema[module]_load") ? "$schema[module]_load" : FALSE, ); return $schema; diff --git js/auto-submit.js js/auto-submit.js new file mode 100644 index 0000000..fa35bfb --- /dev/null +++ js/auto-submit.js @@ -0,0 +1,66 @@ +// $Id: auto-submit.js,v 1.1.2.1 2010/02/17 01:09:46 merlinofchaos Exp $ + +/** + * To make a form auto submit, all you have to do is 3 things: + * + * ctools_add_js('auto-submit'); + * + * On gadgets you want to auto-submit when changed, add the ctools-auto-submit + * class. With FAPI, add: + * @code + * '#attributes' => array('class' => 'ctools-auto-submit'), + * @endcode + * + * Finally, you have to identify which button you want clicked for autosubmit. + * The behavior of this button will be honored if it's ajaxy or not: + * @code + * '#attributes' => array('class' => 'ctools-use-ajax ctools-auto-submit-click'), + * @endcode + * + * Currently only 'select' and 'textfield' types are supported. We probably + * could use additional support for radios and checkboxes. + */ + +Drupal.behaviors.CToolsAutoSubmit = function() { + var timeoutID = 0; + + // Bind to any select widgets that will be auto submitted. + $('select.ctools-auto-submit:not(.ctools-auto-submit-processed)') + .addClass('.ctools-auto-submit-processed') + .change(function() { + $(this.form).find('.ctools-auto-submit-click').click(); + }); + + // Bind to any textfield widgets that will be auto submitted. + $('input[type=text].ctools-auto-submit:not(.ctools-auto-submit-processed)') + .addClass('.ctools-auto-submit-processed') + .keyup(function(e) { + var form = this.form; + switch (e.keyCode) { + case 16: // shift + case 17: // ctrl + case 18: // alt + case 20: // caps lock + case 33: // page up + case 34: // page down + case 35: // end + case 36: // home + case 37: // left arrow + case 38: // up arrow + case 39: // right arrow + case 40: // down arrow + case 9: // tab + case 13: // enter + case 27: // esc + return false; + default: + if (!$(form).hasClass('ctools-ajaxing')) { + if ((timeoutID)) { + clearTimeout(timeoutID); + } + + timeoutID = setTimeout(function() { $(form).find('.ctools-auto-submit-click').click(); }, 300); + } + } + }); +} diff --git plugins/export_ui/ctools_export_ui.class.php plugins/export_ui/ctools_export_ui.class.php new file mode 100644 index 0000000..0976d3c --- /dev/null +++ plugins/export_ui/ctools_export_ui.class.php @@ -0,0 +1,814 @@ +plugin = $plugin; + } + + // ------------------------------------------------------------------------ + // These methods are the API for generating the list of exportable items. + + /** + * Master entry point for handling a list. + * + * It is unlikely that a child object will need to override this method, + * unless the listing mechanism is going to be highly specialized. + */ + function list_page($js, $input) { + ctools_export_load_object_reset($this->plugin['schema']); + // TODO: Probably should be a callback in the schema to handle this in + // case the exports want a more complicated load function. + $this->items = ctools_export_load_object($this->plugin['schema'], 'all'); + + // Respond to a reset command by clearing session and doing a drupal goto + // back to the base URL. + if (isset($input['op']) && $input['op'] == t('Reset')) { + unset($_SESSION['ctools_export_ui'][$this->plugin['name']]); + if (!$js) { + return drupal_goto($_GET['q']); + } + // clear everything but form id, form build id and form token: + $keys = array_keys($input); + foreach ($keys as $id) { + if (!in_array($id, array('form_id', 'form_build_id', 'form_token'))) { + unset($input[$id]); + } + } + $replace_form = TRUE; + } + + // If there is no input, check to see if we have stored input in the + // session. + if (!isset($input['form_id'])) { + if (isset($_SESSION['ctools_export_ui'][$this->plugin['name']]) && is_array($_SESSION['ctools_export_ui'][$this->plugin['name']])) { + $input = $_SESSION['ctools_export_ui'][$this->plugin['name']]; + } + } + else { + $_SESSION['ctools_export_ui'][$this->plugin['name']] = $input; + unset($_SESSION['ctools_export_ui'][$this->plugin['name']]['q']); + } + + // This is where the form will put the output. + $this->rows = array(); + $this->sorts = array(); + + $form_state = array( + 'plugin' => $this->plugin, + 'input' => $input, + 'rerender' => TRUE, + 'no_redirect' => TRUE, + 'object' => &$this, + ); + + ctools_include('form'); + $form = ctools_build_form('ctools_export_ui_list_form', $form_state); + + $output = $this->list_header($form_state) . $this->list_render($form_state) . $this->list_footer($form_state); + + if (!$js) { + $this->list_css(); + return $form . $output; + } + + ctools_include('ajax'); + $commands = array(); + $commands[] = ctools_ajax_command_replace('#ctools-export-ui-list-items', $output); + if (!empty($replace_form)) { + $commands[] = ctools_ajax_command_replace('#ctools-export-ui-list-form', $form); + } + ctools_ajax_render($commands); + } + + /** + * Create the filter/sort form at the top of a list of exports. + * + * This handles the very default conditions, and most lists are expected + * to override this and call through to parent::list_form() in order to + * get the base form and then modify it as necessary to add search + * gadgets for custom fields. + */ + function list_form(&$form, &$form_state) { + // This forces the form to *always* treat as submitted which is + // necessary to make it work. + $form['#token'] = FALSE; + if (empty($form_state['input'])) { + $form["#post"] = TRUE; + } + + // Add the 'q' in if we are not using clean URLs or it can get lost when + // using this kind of form. + if (!variable_get('clean_url', FALSE)) { + $form['q'] = array( + '#type' => 'hidden', + '#value' => $_GET['q'], + ); + } + + $all = array('all' => t('- All -')); + + $form['top row'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + + $form['bottom row'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + + $form['top row']['storage'] = array( + '#type' => 'select', + '#title' => t('Storage'), + '#options' => $all + array( + t('Normal') => t('Normal'), + t('Default') => t('Default'), + t('Overridden') => t('Overridden'), + ), + '#default_value' => 'all', + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['top row']['disabled'] = array( + '#type' => 'select', + '#title' => t('Enabled'), + '#options' => $all + array( + '0' => t('Enabled'), + '1' => t('Disabled') + ), + '#default_value' => 'all', + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['top row']['search'] = array( + '#type' => 'textfield', + '#title' => t('Search'), + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['bottom row']['order'] = array( + '#type' => 'select', + '#title' => t('Sort by'), + '#options' => $this->list_sort_options(), + '#default_value' => 'disabled', + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['bottom row']['sort'] = array( + '#type' => 'select', + '#title' => t('Order'), + '#options' => array( + 'asc' => t('Up'), + 'desc' => t('Down'), + ), + '#default_value' => 'asc', + '#attributes' => array('class' => 'ctools-auto-submit'), + ); + + $form['bottom row']['submit'] = array( + '#type' => 'submit', + '#id' => 'ctools-export-ui-list-items-apply', + '#value' => t('Apply'), + '#attributes' => array('class' => 'ctools-use-ajax ctools-auto-submit-click'), + ); + + $form['bottom row']['reset'] = array( + '#type' => 'submit', + '#id' => 'ctools-export-ui-list-items-apply', + '#value' => t('Reset'), + '#attributes' => array('class' => 'ctools-use-ajax'), + ); + + ctools_add_js('ajax-responder'); + ctools_add_js('auto-submit'); + drupal_add_js('misc/jquery.form.js'); + ctools_add_js('export-ui-list.js'); + + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + } + + /** + * Validate the filter/sort form. + * + * It is very rare that a filter form needs validation, but if it is + * needed, override this. + */ + function list_form_validate(&$form, &$form_state) { } + + /** + * Submit the filter/sort form. + * + * This submit handler is actually responsible for building up all of the + * rows that will later be rendered, since it is doing the filtering and + * sorting. + * + * For the most part, you should not need to override this method, as the + * fiddly bits call through to other functions. + */ + function list_form_submit(&$form, &$form_state) { + // Filter and re-sort the pages. + $plugin = $this->plugin; + + $prefix = ctools_export_ui_plugin_base_path($plugin['name']); + + foreach ($this->items as $name => $item) { + // Call through to the filter and see if we're going to render this + // row. If it returns TRUE, then this row is filtered out. + if ($this->list_filter($form_state, $item)) { + continue; + } + + $allowed_operations = drupal_map_assoc(array_keys($plugin['allowed operations'])); + $not_allowed_operations = array(); + + if ($item->type == t('Normal')) { + $not_allowed_operations[] = 'revert'; + } + elseif ($item->type == t('Overridden')) { + $not_allowed_operations[] = 'delete'; + } + else { + $not_allowed_operations[] = 'revert'; + $not_allowed_operations[] = 'delete'; + } + + $not_allowed_operations[] = empty($item->disabled) ? 'enable' : 'disable'; + + foreach ($not_allowed_operations as $op) { + // Remove the operations that are not allowed for the specific + // exportable. + unset($allowed_operations[$op]); + } + + $operations = array(); + + foreach ($allowed_operations as $op) { + $operations[$op] = array( + 'title' => $plugin['allowed operations'][$op], + 'href' => $prefix . '/' . str_replace('%ctools_export_ui', $name, $plugin['menu']['items'][$op]['path']), + ); + if (in_array($op, array('enable', 'disable'))) { + $operations[$op]['attributes'] = array('class' => 'ctools-use-ajax'); + } + } + + $this->list_build_row($item, $form_state, $operations); + } + + // Now actually sort + if ($form_state['values']['sort'] == 'desc') { + arsort($this->sorts); + } + else { + asort($this->sorts); + } + + // Nuke the original. + $rows = $this->rows; + $this->rows = array(); + // And restore. + foreach ($this->sorts as $name => $title) { + $this->rows[$name] = $rows[$name]; + } + } + + /** + * Determine if a row should be filtered out. + * + * This handles the default filters for the export UI list form. If you + * added additional filters in list_form() then this is where you should + * handle them. + * + * @return + * TRUE if the item should be excluded. + */ + function list_filter($form_state, $item) { + if ($form_state['values']['storage'] != 'all' && $form_state['values']['storage'] != $item->type) { + return TRUE; + } + + if ($form_state['values']['disabled'] != 'all' && $form_state['values']['disabled'] != !empty($item->disabled)) { + return TRUE; + } + + if ($form_state['values']['search']) { + $search = strtolower($form_state['values']['search']); + foreach ($this->list_search_fields() as $field) { + if (strpos(strtolower($item->$field), $search) !== FALSE) { + $hit = TRUE; + break; + } + } + if (empty($hit)) { + return TRUE; + } + } + } + + /** + * Provide a list of fields to test against for the default "search" widget. + * + * This widget will search against whatever fields are configured here. By + * default it will attempt to search against the name, title and description fields. + */ + function list_search_fields() { + $fields = array( + $this->plugin['export']['key'], + ); + + if (!empty($this->plugin['export']['admin_title'])) { + $fields[] = $this->plugin['export']['admin_title']; + } + if (!empty($this->plugin['export']['admin_description'])) { + $fields[] = $this->plugin['export']['admin_description']; + } + + return $fields; + } + + /** + * Provide a list of sort options. + * + * Override this if you wish to provide more or change how these work. + * The actual handling of the sorting will happen in build_row(). + */ + function list_sort_options() { + if (!empty($this->plugin['export']['admin_title'])) { + $options = array( + 'disabled' => t('Enabled, title'), + $this->plugin['export']['admin_title'] => t('Title'), + ); + } + else { + $options = array( + 'disabled' => t('Enabled, name'), + ); + } + + $options += array( + 'name' => t('Name'), + 'storage' => t('Storage'), + ); + + return $options; + } + + /** + * Add listing CSS to the page. + * + * Override this if you need custom CSS for your list. + */ + function list_css() { + ctools_add_css('export-ui-list'); + } + + /** + * Build a row based on the item. + * + * By default all of the rows are placed into a table by the render + * method, so this is building up a row suitable for theme('table'). + * This doesn't have to be true if you override both. + */ + function list_build_row($item, &$form_state, $operations) { + // Set up sorting + $name = $item->{$this->plugin['export']['key']}; + + // Note: $item->type should have already been set up by export.inc so + // we can use it safely. + switch ($form_state['values']['order']) { + case 'disabled': + $this->sorts[$name] = empty($item->disabled) . $name; + break; + case 'title': + $this->sorts[$name] = $item->{$this->plugin['export']['admin_title']}; + break; + case 'name': + $this->sorts[$name] = $name; + break; + case 'storage': + $this->sorts[$name] = $item->type . $name; + break; + } + + $class = + + $this->rows[$name]['data'] = array(); + $this->rows[$name]['class'] = !empty($item->disabled) ? 'ctools-export-ui-disabled' : 'ctools-export-ui-enabled'; + + // If we have an admin title, make it the first row. + if ($this->plugin['export']['admin_title']) { + $this->rows[$name]['data'][] = array('data' => check_plain($item->{$this->plugin['export']['admin_title']}), 'class' => 'ctools-export-ui-title'); + } + $this->rows[$name]['data'][] = array('data' => check_plain($name), 'class' => 'ctools-export-ui-name'); + $this->rows[$name]['data'][] = array('data' => check_plain($item->type), 'class' => 'ctools-export-ui-storage'); + $this->rows[$name]['data'][] = array('data' => theme('links', $operations), 'class' => 'ctools-export-ui-operations'); + + // Add an automatic mouseover of the description if one exists. + if (!empty($this->plugin['export']['admin_description'])) { + $this->rows[$name]['title'] = $item->{$this->plugin['export']['admin_description']}; + } + } + + /** + * Provide the table header. + * + * If you've added columns via list_build_row() but are still using a + * table, override this method to set up the table header. + */ + function list_table_header() { + $header = array(); + if ($this->plugin['export']['admin_title']) { + $header[] = array('data' => t('Title'), 'class' => 'ctools-export-ui-title'); + } + + $header[] = array('data' => t('Name'), 'class' => 'ctools-export-ui-name'); + $header[] = array('data' => t('Storage'), 'class' => 'ctools-export-ui-storage'); + $header[] = array('data' => t('Operations'), 'class' => 'ctools-export-ui-operations'); + + return $header; + } + + /** + * Render all of the rows together. + * + * By default we place all of the rows in a table, and this should be the + * way most lists will go. + * + * Whatever you do if this method is overridden, the ID is important for AJAX + * so be sure it exists. + */ + function list_render(&$form_state) { + return theme('table', $this->list_table_header(), $this->rows, array('id' => 'ctools-export-ui-list-items')); + } + + /** + * Render a header to go before the list. + * + * This will appear after the filter/sort widgets. + */ + function list_header($form_state) { } + + /** + * Render a footer to go after thie list. + * + * This is a good place to add additional links. + */ + function list_footer($form_state) { } + + // ------------------------------------------------------------------------ + // These methods are the API for adding/editing exportable items + + function add_page($js, $input) { + $form_state = array( + 'plugin' => $this->plugin, + 'object' => &$this, + 'ajax' => $js, + 'item' => ctools_export_new_object($this->plugin['schema']), + 'op' => 'add', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + ); + + return $this->edit_execute_form($form_state); + } + + /** + * Main entry point to edit an item. + * + * The default implementation simply uses a form, so this should be + * overridden for more complex implentations that need more than to display + * a simple form (like a view or a page manager page). + */ + function edit_page($js, $input, $item) { + $form_state = array( + 'plugin' => $this->plugin, + 'object' => &$this, + 'ajax' => $js, + 'item' => $item, + 'op' => 'edit', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + ); + + return $this->edit_execute_form($form_state); + } + + function clone_page($js, $input, $item) { + // To make a clone of an item, we first export it and then re-import it. + // Export the handler, which is a fantastic way to clean database IDs out of it. + $schema = ctools_export_get_schema($this->plugin['schema']); + ctools_include('export'); + $export = ctools_export_object($this->plugin['schema'], $item); + + ob_start(); + eval($export); + ob_end_clean(); + + if (empty(${$schema['export']['identifier']})) { + return drupal_not_found(); + } + + $item = ${$schema['export']['identifier']}; + $item->{$this->plugin['export']['key']} = 'clone_of_' . $item->name; + + // Set these defaults just the same way that ctools_export_new_object sets them. + $item->export_type = EXPORT_IN_DATABASE; + $item->type = t('Local'); + + $form_state = array( + 'plugin' => $this->plugin, + 'object' => &$this, + 'ajax' => $js, + 'item' => $item, + 'op' => 'add', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + ); + + return $this->edit_execute_form($form_state); + } + + /** + * Execute the form. + * + * Add and Edit both funnel into this, but they have a few different + * settings. + */ + function edit_execute_form($form_state) { + ctools_include('form'); + $output = ctools_build_form('ctools_export_ui_edit_item_form', $form_state); + if (!empty($form_state['executed'])) { + $item = &$form_state['item']; + $export_key = $this->plugin['export']['key']; + + $schema = ctools_export_get_schema($this->plugin['schema']); + $result = $schema['export']['save callback']($item); + + if ($result) { + drupal_set_message(t('Saved @plugin %title.', array('@plugin' => $this->plugin['title'], '%title' => $item->{$export_key}))); + } + else { + drupal_set_message(t('Could not save @plugin %title.', array('@plugin' => $this->plugin['title'], '%title' => $item->{$export_key})), 'error'); + } + } + + return $output; + } + + /** + * Provide the actual editing form. + */ + function edit_form(&$form, &$form_state) { + $export_key = $this->plugin['export']['key']; + $item = $form_state['item']; + + // Set title. + if ($form_state['op'] == 'edit') { + if ($item->export_type & EXPORT_IN_DATABASE) { + $title_op = $form_state['op']; + } + else { + $title_op = 'view'; + } + } + else { + $title_op = $form_state['op']; + } + + // Replace %title that might be there with the exportable title. + drupal_set_title(str_replace('%title', $item->{$export_key}, $this->plugin['form']['string']['title'][$title_op])); + + // TODO: Drupal 7 has a nifty method of auto guessing names from + // titles that is standard. We should integrate that here as a + // nice standard. + // Guess at a couple of our standard fields. + if (!empty($this->plugin['export']['admin_title'])) { + $form['info'][$this->plugin['export']['admin_title']] = array( + '#type' => 'textfield', + '#title' => t('Administrative title'), + '#description' => t('This will appear in the administrative interface to easily identify it.'), + '#default_value' => $item->{$this->plugin['export']['admin_title']}, + ); + } + + $form['info'][$export_key] = array( + // TODO: Add human readable name on key in export.inc? + '#title' => ucfirst($export_key), + '#type' => 'textfield', + '#default_value' => $item->{$export_key}, + '#description' => t('The unique ID for this @export', array('@export' => $this->plugin['title'])), + '#required' => TRUE, + '#maxlength' => 255, + ); + + if ($form_state['op'] === 'edit') { + $form['info'][$export_key]['#disabled'] = TRUE; + $form['info'][$export_key]['#value'] = $item->{$export_key}; + } + else { + $form['info'][$export_key]['#element_validate'] = array('ctools_export_ui_edit_name_validate'); + } + + if (!empty($this->plugin['export']['admin_description'])) { + $form['info'][$this->plugin['export']['admin_description']] = array( + '#type' => 'textarea', + '#title' => t('Administrative description'), + '#default_value' => $item->{$this->plugin['export']['admin_description']}, + ); + } + + // Add plugin's form definitions. + if (!empty($this->plugin['form']['settings'])) { + // Pass $form by reference. + $this->plugin['form']['settings']($form, $this->plugin, $form_state['op'], $item); + } + + + // Make sure that whatever happens, the buttons go to the bottom. + $form['buttons']['#weight'] = 100; + + // Add buttons. + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + $form['buttons']['delete'] = array( + '#type' => 'submit', + '#value' => $item->export_type & EXPORT_IN_CODE ? t('Revert') : t('Delete'), + '#access' => $form_state['op'] === 'edit' && $item->export_type & EXPORT_IN_DATABASE, + '#submit' => 'ctools_export_ui_edit_name_validate', + ); + + } + + function edit_form_validate(&$form, &$form_state) { } + + /** + * Handle the submission of the edit form. + * + * At this point, submission is successful. Our only responsibility is + * to copy anything out of values onto the item that we are able to edit. + * + * If the keys all match up to the schema, this method will not need to be + * overridden. + */ + function edit_form_submit(&$form, &$form_state) { + $plugin = $form_state['plugin']; + $export_key = $plugin['export']['key']; + $item = $form_state['item']; + + $schema = ctools_export_get_schema($plugin['schema']); + foreach (array_keys($schema['fields']) as $key) { + if(isset($form_state['values'][$key])) { + $item->{$key} = $form_state['values'][$key]; + } + } + } + + // ------------------------------------------------------------------------ + // These methods are the API for 'other' stuff with exportables such as + // enable, disable, import, export + + /** + * Callback to enable a page. + */ + function enable_page($js, $input, $item) { + return $this->set_item_state(FALSE, $js, $input, $item); + } + + /** + * Callback to disable a page. + */ + function disable_page($js, $input, $item) { + return $this->set_item_state(TRUE, $js, $input, $item); + } + + /** + * Set an item's state to enabled or disabled and output to user. + * + * If javascript is in use, this will rebuild the list and send that back + * as though the filter form had been executed. + */ + function set_item_state($state, $js, $input, $item) { + ctools_export_set_object_status($item, $state); + + if (!$js) { + drupal_goto(ctools_export_ui_plugin_base_path($this->plugin['name'])); + } + else { + return $this->list_page($js, $input); + } + } + + function export_page() { + + } + + function import_page() { + + } + +} + +// ----------------------------------------------------------------------- +// Forms to be used with this class. +// +// Since Drupal's forms are completely procedural, these forms will +// mostly just be pass-throughs back to the object. + +/** + * Form callback to handle the filter/sort form when listing items. + * + * This simply loads the object defined in the plugin and hands it off. + */ +function ctools_export_ui_list_form(&$form_state) { + $form = array(); + $form_state['object']->list_form($form, $form_state); + return $form; +} + +/** + * Validate handler for ctools_export_ui_list_form. + */ +function ctools_export_ui_list_form_validate(&$form, &$form_state) { + $form_state['object']->list_form_validate($form, $form_state); +} + +/** + * Submit handler for ctools_export_ui_list_form. + */ +function ctools_export_ui_list_form_submit(&$form, &$form_state) { + $form_state['object']->list_form_submit($form, $form_state); +} + +/** + * Form callback to edit an exportable item. + * + * This simply loads the object defined in the plugin and hands it off. + */ +function ctools_export_ui_edit_item_form(&$form_state) { + $form = array(); + $form_state['object']->edit_form($form, $form_state); + return $form; +} + +/** + * Validate handler for ctools_export_ui_edit_item_form. + */ +function ctools_export_ui_edit_item_form_validate(&$form, &$form_state) { + $form_state['object']->edit_form_validate($form, $form_state); +} + +/** + * Submit handler for ctools_export_ui_edit_item_form. + */ +function ctools_export_ui_edit_item_form_submit(&$form, &$form_state) { + $form_state['object']->edit_form_submit($form, $form_state); +} + +/** + * Submit handler to delete for ctools_export_ui_edit_item_form + */ +function ctools_export_ui_edit_item_form_delete(&$form, &$form_state) { + $plugin = $form_state['plugin']; + $export_key = $plugin['export']['key']; + $item = $form_state['item']; + + $menu_prefix = ctools_export_ui_plugin_base_path($plugin['name']); + $form_state['redirect'] = "$menu_prefix/list/$item->{$export_key}/delete"; +} + +/** + * Validate that an export item name is acceptable and unique during add. + */ +function ctools_export_ui_edit_name_validate($element, &$form_state) { + $plugin = $form_state['plugin']; + // Check for string identifier sanity + if (!preg_match('!^[a-z0-9_]+$!', $element['#value'])) { + form_error($element, t('The export id can only consist of lowercase letters, underscores, and numbers.')); + return; + } + // Check for name collision + $schema = ctools_export_get_schema($plugin['schema']); + + if ($exists = call_user_func($schema['export']['load callback'], $element['#value'])) { + form_error($element, t('A @plugin with this name already exists. Please choose another name or delete the existing export before creating a new one.', array('@plugin' => $plugin['title']))); + } +} diff --git plugins/export_ui/ctools_export_ui.inc plugins/export_ui/ctools_export_ui.inc new file mode 100644 index 0000000..1f93710 --- /dev/null +++ plugins/export_ui/ctools_export_ui.inc @@ -0,0 +1,19 @@ + array( + 'class' => 'ctools_export_ui', + ), + // As this is the base class plugin, it shouldn't declare any menu items. + 'has menu' => FALSE, +);