Merging in branch 'DRUPAL-6--1', with many manual conflict resolutions.
authorSam Boyer
Tue, 7 Sep 2010 09:02:51 +0000 (09:02 +0000)
committerSam Boyer
Tue, 7 Sep 2010 09:02:51 +0000 (09:02 +0000)
     Conflicts:
         CHANGELOG.txt
         css/modal.css
         ctools.module
         ctools_plugin_example/plugins/access/arg_length.inc
         ctools_plugin_example/plugins/access/example_role.inc
         help/export.html
         includes/ajax.inc
         includes/cleanstring.inc
         includes/content.inc
         includes/content.menu.inc
         includes/context-task-handler.inc
         includes/context.inc
         includes/context.theme.inc
         includes/css.inc
         includes/export.inc
         includes/modal.inc
         includes/object-cache.inc
         includes/plugins.inc
         includes/wizard.inc
         js/ajax-responder.js
         js/collapsible-div.js
         js/dependent.js
         js/dimensions.js
         js/mc.js
         js/modal.js
         page_manager/page_manager.admin.inc
         page_manager/page_manager.module
         page_manager/plugins/tasks/page.inc
         page_manager/plugins/tasks/search.inc
         page_manager/plugins/tasks/user_view.inc
         plugins/access/context_exists.inc
         plugins/access/node_language.inc
         plugins/access/perm.inc
         plugins/access/php.inc
         plugins/access/role.inc
         plugins/access/site_language.inc
         plugins/access/term.inc
         plugins/access/term_vocabulary.inc
         plugins/content_types/block/block.inc
         plugins/content_types/custom/custom.inc
         plugins/content_types/node/node.inc
         plugins/content_types/node_context/node_comment_form.inc
         plugins/content_types/node_context/node_comments.inc
         plugins/content_types/node_context/node_content.inc
         plugins/content_types/page/page_breadcrumb.inc
         plugins/content_types/page/page_footer_message.inc
         plugins/content_types/page/page_help.inc
         plugins/content_types/page/page_messages.inc
         plugins/content_types/page/page_mission.inc
         plugins/content_types/page/page_slogan.inc
         plugins/content_types/page/page_tabs.inc
         plugins/content_types/page/page_title.inc
         plugins/content_types/search/search_result.inc
         plugins/contexts/node_edit_form.inc
         plugins/contexts/term.inc
         plugins/contexts/terms.inc
         plugins/contexts/user.inc
         plugins/relationships/book_parent.inc
         views_content/plugins/content_types/views.inc
         views_content/plugins/content_types/views_panes.inc
         views_content/views_content.module

90 files changed:
API.txt
CHANGELOG.txt
bulk_export/bulk_export.module
css/ctools.css
css/modal.css
ctools.install
ctools.module
ctools_plugin_example/plugins/access/arg_length.inc
ctools_plugin_example/plugins/access/example_role.inc
help/ctools.help.ini
help/export.html
help/plugins-creating.html
help/plugins.html
help/wizard.html
includes/ajax.inc
includes/cleanstring.inc
includes/content.inc
includes/content.menu.inc
includes/context-access-admin.inc
includes/context-admin.inc
includes/context-task-handler.inc
includes/context.inc
includes/context.theme.inc
includes/css.inc
includes/export.inc
includes/form.inc
includes/menu.inc
includes/modal.inc
includes/object-cache.inc
includes/plugins.inc
includes/wizard.inc
js/ajax-responder.js
js/collapsible-div.js
js/modal.js
page_manager/page_manager.admin.inc
page_manager/page_manager.install
page_manager/page_manager.module
page_manager/plugins/tasks/page.admin.inc
page_manager/plugins/tasks/page.inc
page_manager/plugins/tasks/search.inc
page_manager/plugins/tasks/term_view.inc
page_manager/plugins/tasks/user_view.inc
plugins/access/context_exists.inc
plugins/access/node_language.inc
plugins/access/perm.inc
plugins/access/php.inc
plugins/access/role.inc
plugins/access/site_language.inc
plugins/access/term_vocabulary.inc
plugins/arguments/term.inc
plugins/content_types/block/block.inc
plugins/content_types/custom/custom.inc
plugins/content_types/node/node.inc
plugins/content_types/node_context/node_body.inc
plugins/content_types/node_context/node_comment_form.inc
plugins/content_types/node_context/node_comments.inc
plugins/content_types/node_context/node_content.inc
plugins/content_types/node_context/node_updated.inc
plugins/content_types/node_form/node_form_attachments.inc
plugins/content_types/node_form/node_form_author.inc
plugins/content_types/node_form/node_form_book.inc
plugins/content_types/node_form/node_form_buttons.inc
plugins/content_types/node_form/node_form_comment.inc
plugins/content_types/node_form/node_form_input_format.inc
plugins/content_types/node_form/node_form_menu.inc
plugins/content_types/node_form/node_form_path.inc
plugins/content_types/node_form/node_form_publishing.inc
plugins/content_types/node_form/node_form_taxonomy.inc
plugins/content_types/page/page_breadcrumb.inc
plugins/content_types/page/page_footer_message.inc
plugins/content_types/page/page_help.inc
plugins/content_types/page/page_messages.inc
plugins/content_types/page/page_mission.inc
plugins/content_types/page/page_slogan.inc
plugins/content_types/page/page_tabs.inc
plugins/content_types/page/page_title.inc
plugins/content_types/search/search_result.inc
plugins/content_types/term_context/term_list.inc
plugins/contexts/node.inc
plugins/contexts/node_add_form.inc
plugins/contexts/node_edit_form.inc
plugins/contexts/term.inc
plugins/contexts/user.inc
plugins/contexts/vocabulary.inc
plugins/relationships/book_parent.inc
views_content/plugins/content_types/views.inc
views_content/plugins/content_types/views_panes.inc
views_content/plugins/views/views_content.views.inc
views_content/plugins/views/views_content_plugin_display_panel_pane.inc
views_content/views_content.module

diff --git a/API.txt b/API.txt
index aab1153..51111c0 100644 (file)
--- a/API.txt
+++ b/API.txt
@@ -1,6 +1,81 @@
 API.txt: $Id$
 
 This file contains a log of changes to the API.
+API version 1.8
+  Introduce 'get base contexts' callback to all tasks to facilitate
+    Page Manager's site template extracting contexts from the environment.
+  Introduce 'ctools_content_editable' function and the corresponding
+    'check editable' callback on all content types to determine if a
+    content type can be edited based upon configuration.
+  Introduce utility.inc and move some code to it.
+  Introduce page-wizard.inc and a "page_wizard" type plugin for Page Manager.
+
+API version 1.7.2
+  Allow Export UI to automatically have wizards by setting 'use wizard' => TRUE
+  Moved the Panels Stylizer module into CTools so that the UI can be
+    used to manage styles for modules other than Panels.
+  Introduce ctools_access_ruleset module for customizable access rulesets.
+  Introduce ctools_custom_content module for customizable content panes.
+  Add 'convert default' as a converter keyword for context implementations.
+    Instead of reverting $context->title this will cause %context without
+    :converter to use a default converter.
+  
+API version 1.7.1
+  Introduce 'modal return' and 'ajax return' to wizard.inc to allow
+    wizard owners to process their own ajax/modal output.
+  Introduce math-expr.inc to allow simple math expression parsing.
+
+API version 1.7
+  Introduce the export-ui plugin. This introduces new keys to the 'export'
+    section of schema: 'primary key', 'key name'.
+  Introduce ctools_export_crud_ functions to provide CRUD access to most
+    exportables. This introduces several optional callback keys to the
+    'export' section of the schema to provide overrides.
+  Introduce auto-submit.js. Forms may now be set to auto submit just by
+    adding appropriate classes.
+  Provide new default functions for bulk export. This may make items that
+    were previously not bulk exportable due to missing 'list' callback
+    appear in the bulk export UI.
+  
+API version 1.5
+  Add two new alter hooks: page_manager_operations_alter and 
+      page_manager_variant_operations_alter to allow modules to add tabs
+      to any page manager page.
+
+API version 1.4:
+  Allow themes to provide APIs which includes default pages of all types.
+  Intorduce ctools_css_add_css() to allow private file systems to have generated CSS.
+  Introduce initial build of stylizer.inc to allow UI configurable styles.
+  Introduce 'cache warming' feature. Use 'ctools-use-ajax-cache' or
+    'ctools-use-modal-cache'. Doing so will cause content to be fetched
+    via AJAX on page load and kept warm in a cache for instant responses
+    to clicks.
+  Generalized ctools_add_css().
+  Generalized ctools_add_js().
+  Generalized ctools_image_path().
+  Make global hooks for plugin definition optional through a 'use hooks'
+    plugin option.
+
+API version 1.3.2:
+  Introduce 'export callback' to individual fields in export.inc
+
+API version 1.3.1:
+  #649144 by neclimdul: Expand ctools_include() to work for other modules.
+
+API version 1.3:
+  Introduce ctools_jump_menu().
+  Change plugins to no longer need magic function. Can now use $plugin = array(...) instead.
+
+API version 1.2:
+  Introduce ctools_set_variable_token().
+  Introduce ctools_set_callback_token().
+  Introduce cleanstring tool. See cleanstring.inc
+  Introduce page_manager_get_current_page().
+  Introduce ctools_ajax_command_redirect().
+  Introduce ctools_ajax_command_reload().
+  Introduce ctools_ajax_command_submit().
+  Introduce ctools_static().
+  Introduce ctools_modal_command_loading().
 
 API version 2.0.0
 
index ca4174c..b7ef8c4 100644 (file)
@@ -1,4 +1,4 @@
-Current API VERSION: 1.3. See API.txt for more information.
+Current API VERSION: 1.7.2. See API.txt for more information.
 
 ctools 7.x-1.x-dev
 ==================
@@ -38,9 +38,174 @@ Change plugins to no longer need magic function. Can now use $plugin = array(...
 #604976: Prevent Page Manager from throwing errors when no modules that provide variants are available.
 Add 'Update and save' button to reduce the number of needed steps to do basic content editing.
 #693742: Sanitize block admin output (to Panels drag & drop for example) to get rid of script tags.
+#856636: Fix warnings on update.
+#866524: "Custom" content type could cause crash if trying to reuse it without activating the custom content type module.
+#870820 by EclipseGc: ctools_export_crud_import() passed wrong arguments to import callback, if defined.
+Improve the task system to make it possible for Panels Everywhere to get more contexts.
+#718368: Ignore query string when testing js and css files for inclusion during AJAX requests.
+#690648 by awolfey: User signature content type.
+#874960 by yhahn: Export UI goes to wrong path on revert.
+#868410: Make transparency work on modal backdrop in IE.
+#872340: AJAX framework used wrong URLs with i18n and CSS files.
+#609246: Create variant to handle 403, 404 and 301 redirects for non-page page responses.
+#870466: Invisible steps broken by too aggressive of a test for valid wizard steps. This broke Mini Panel layout change in Panels.
+#872804: Export UI delete query failing to get table prefix thanks to PHP oddity.
+#867864: Fix a admin title and edit link problems with custom content types.
+#812744: Did not quite get query string removal working earlier, causing random breakages to AJAX requests.
+#866622 by dkinzer: Fix dimensions.js to not crash on undefined items.
+#215927: Allow exposed form blocks to inherit the current path.
+#875020: Remove defaults for "API" in export section of schema as "No API version" is a possible option.
+Activating export-ui based modules caused warnings (that did not actually cause problems) on the modules page.
+#887812 by ayalon: Fix javascript crash bug.
+#848580: Re-organize form submission in modals so that wysiwyg can detach upon submit.
+#872072: Allow the modal to be vastly more customized.
+#873616: Invalid SQL in term_parent access plugin.
+#883490: Page manager "page" task needed to validate that % was not used in the first part of a path as that is not supported by Drupal menu system.
+#883914: Ensure content pane validity before attemping to use.
+#884092: Fix stylizer error message that warns you when there are no modules utilizing stylizer.
+#831922 by tizzo: Make ctools_build_form() better support #ahah by properly rebuilding forms when necessary.
+#867554: Accidentally binding modal javascript to buttons that were not inputs outside of the modal.
+#879438: Change require_once to include_once when including plugin files in case plugins have disappeared.
+#896660 by zoo33: Export-UI used wrong object name in descript of export key field.
+New "Page wizards" to more easily create well-known types of pages in Page Manager.
+#891682 by Dave Reid: Have modal title use the page title if no title was set on the form.
 
-ctools 6.x-1.x-dev
-==================
+ctools 6.x-1.7 (2010-Jul-26)
+==============
+Deprecated the callback-based 'defaults' in favor of the identical 'process' (plugin system). Consequently, moved the content_type plugin over to use 'process' instead of the 'defaults' callback.
+Added a ctools.api.php file and began documenting some of our api/hooks there.
+Changed all hook_ctools_plugin_directory() implementations to use the more informative variable naming scheme.
+#817612 by elstudio: Fix validation error that was making it impossible to add 'Existing node' content_types.
+#787644 by Amitaibu and merlinofchaos: Introduce export-ui and auto-submit. See API.txt for more.
+Update the plugin system documentation, and clean up plugin system logic.
+#827498: Allow the 'custom' content type to be re-usable and exportable.
+#827370 by dereine: Allow the new auto-submit.js to allow a single class that causes every element in the form to auto-submit.
+#717036: Add a check to prevent CSS cache files from being cleared on a cron run.
+Partial reversion of #742832.
+#767046: Current theme selection rule was broken.
+Improve the "Edit panel" tab so that Panels Everywhere can use a different name for site template, and Panels can direct it to the most appropriate tab in the editor.
+#828352: Recent improvement to allow selection of fields to display in pane caused summary styles to break.
+#764006 by drifter: Fix warning when using Panels Everywhere and views content panes together.
+#467898 by mikeker: Allow taxonomy term argument to be restricted to a particular set of vocabularies.
+#829582 by dmitrig01: apostrophes as data keys in exports would not properly be escaped, breaking the export.
+#845540 by Dave Reid: cleanstring.inc had accidental dependency on pathauto.
+#755954 by killes: Add relationship plugin to get node edit form context from node context.
+#755988 by killes: Render form id, build id and token as part of the form buttons context.
+#756118 by killes: node_form_author.inc sets content regardless of content being present.
+#783408 by hefox: Make sure empty arguments are properly translated to NULL when passed to Views.
+#749398 by harijari: Properly support new column added for signatures in Drupal 6.13.
+#815164 by DeFR: Replace fragile server side cache of aggregated .js and .css with more robust embedded .js communicating this information.
+#667504 by Jen Lampton: Allow node title to be linked to the node.
+#830274 by Amitaibu: Wrong value passed to ctools_export_crud_load_all() by the default UI plugin.
+#843280 by Amitaibu: Export UI would WSOD on delete.
+#728486 by elliotttf: Fix PHP 5.3 compatibility.
+#635730 by dereine and merlinofchaos: Fix page title pane to show actual title. Allow the tag and tag class and id to be specified in pane settings.
+#842882 by Amitaibu: Safer handling of determing if an object is new or updated when using ctools_export_crud_save().
+#826074 by Amitaibu: Provide more defaults in schema for new Export UI settings.
+#715546 by jsfwd: Term list content type didn't get proper first/last classes.
+#731950: Prevent warning if task has invalid subtasks callback.
+#831592: Context: Taxonomy vocabulary would not save vocabulary value.
+#836828 by jmiccolis: ctools_include() would try to include again even though it was trying to statically cache a list of files already included.
+#843042: export-ui delete button did not work.
+#847682 by dagmar: Allow export.inc controlled objects to have data that exists in sibling tables to enable integration with exportables.module.
+Allow Views to use their exposed filters as pane configuration. This is under the "Allow" settings in the Content pane display.
+#849418: "Custom" content type was accidentally losing context substitutions created when reusable custom content types went in.
+Moved Panels stylizer UI to CTools. If you use Panels Stylizer, be sure to update Panels at the same time.
+Introduce ctools_access_ruleset module for customizable access rulesets.
+Introduce ctools_custom_content module for customizable content panes and move the associated UI code to the module.
+Restore a less aggressive plugin caching.
+#854190 by Amitaibu: Provide a nicer experience with stylizer if there are no stylizer-enabled modules running.
+#737602: Generic NOT checkbox for all access tests.
+#735922 by daniboy: Allow the redirect command to have a delay.
+#704132: Fix broken term_parent access plugin.
+#596212 by KoCo: Fix warning message if using taxonomy terms context but no valid terms were found.
+#657652: PHP Access rules become uneditable if they had no description.
+#680778 by hefox: Taxonomy synonyms not showing correctly in term list content type.
+#617678: Improve 'back' button handling during adding a page in page manager.
+#817810: Search menu retooling caused the title of search pages to get lost.
+#505132: Fix CSS caching to truly work on private filesystem now.
+#707990: Add default converters and a default converter to user context to ease integration with realname.module.
+#592986: Allow "text-transform" in user-generated CSS.
+#507092: Add a "view" context, meaning you can load a view into a context and then display individual rows from that view as panes.
+#696402 by pokurek: Prevent node add/edit contexts from getting into infinite loops.
+#609424: Book relationship plugin was completely non-functional.
+#709242: Required contexts would lose keyword and identifier when transfering context from one system into the subsystem.
+#860306 by jcmarco: "Substitutions" fieldset would not open due to missing js.
+#861778 by ayalon: Hold session ID for anonymous users when using the object cache so wizards can work for anonymous.
+#846408: Increase #delta in reorder weights so that more than 21 variants can be used.
+#827310: Node comments content type should not display comments if the node is configured to disable them.
+#622570 by omerida: Add an option to display the comment forbidden information if the comment form cannot be displayed in comment form content type.
+#863296: Do not leave NULL plugins left by searching for nonexistant plugins in the list when getting all plugins.
+#853256 by swentel: Allow the normal node view content types to support CCK hosted build modes like Views does.
+#865392 by Amitaibu: Auto-guess ctools ajax class with element associations.
+
+ctools 6.x-1.6 (2010-Jun-1)
+==============
+(No changes - 1.5 release was improperly tagged)
+
+ctools 6.x-1.5 (2010-May-28)
+==============
+#804198: CTools AJAX framework did not work properly with javascript aggregation.
+Handle forms passing through required contexts properly.
+Add operation alter tooks to let modules add items to Page Manager pages.
+Partial rollback of #711664 - central-hook-based plugin definition no longer defaults to FALSE, as this caused widespread unexpected behavior in modules using ctools plugins.
+#708926: Remove dependency-busting call to panels_get_path().
+Do not allow Views Content Pane displays to be selected by the legacy Views content connector; based on #791960.
+#737434 by nealeyoung: Make the node_updated content_type plugin actually do what it advertises and output last change time.
+
+ctools 6.x-1.4 (2010-May-19)
+==============
+#686764: Update page tokens to use actual page rendering mechanism rather than the tokens which causes them to appear as having content even when empty.
+Retool the page elements to render last and not use the token method.
+Allow themes to provide default pages so that Panels Everywhere enabled themes can provide layout variants for the site template.
+Initial inclusion of the stylizer plugin to create user customizable styles from pre-configured base styles.
+#708154: Update and Save would leave forms in an older state, causing some forms to lose data (particularly the Panels content form).
+#467948 by hefox: Allow the vocabulary context to be used by the vocabulary selector access plugin.
+#686052 by Scott Reynolds: Allow for cache warming. (See API.txt)
+#686726 by ayalon: Access plugin to mimic the block path configuration item.
+#484340 by catch: Fix bug in token integration.
+#709754 by yhahn: Add export_module flag to all default objects so that we can tell where they came from.
+#709840 by thsutton: Improper test of AND when displaying access summaries.
+#709874 by thsutton: Fix improper test in context exists.
+#707826 by marcvangend: Add a relationship to get multiple terms from a node.
+#711922 by jonskulski: Do not print empty H1 tag if no title in page_title content.
+#703040 by neclimudul: Harden plugin loading against accidental variable overwriting.
+#710490 by thsutton: Fix use of error_get_last() which only appears in PHP 5.2.
+#716288 by alex_b: Clear more caches when clearing static caches for install/deployment purposes.
+#715118 by neclimdul: Add abstract hints to the classes in plugins.inc.
+#723296 by andrewlevine: Generalize more of CTools helpers so they can be used for non-CTools files.
+#726320 by gordon: Allow the CTools wizard to have query strings in the wizard path.
+#711664 by meclimdul: Make the 'hook' version of plugins now optional and improve code around it.
+#745468 by alevine and Scott Reynolds: Make ajax better able to know what .js/.css was already on the page.
+#722246: Search tabs not quite right, particularly with retaining keywords across tabs. This mostly fixes.
+#709754 followup by yhahn: Improved documentation for export.inc
+#565808 by cha0s, zroger and davereid: Backport nojs handling from D7.
+#704132 by killes: Allow access/selection rule based on whether taxonomy term has a parent.
+#747588 by mikeker: Add taxonomy description to context tokens.
+#741588 by Jody Lynn: add db_rewrite_sql() to comments pane query to match core.
+#531366 by mikeker: Move token substitutions to prior to filtering in custom content type. This could have some effects on existing installs.
+#718028 by redben: Improved documentation for wizard.inc
+#750004 by jhedstrom: Comment pane ignores node comment display setting.
+#752960 by mgriego: Redirect destination on 'delete' in admin links could be wrong.
+#754234 by killes: Node form content types not showing up due to logic error.
+#754594 by lavamind: Add a "book children" content type.
+Ensure imagefilter() exists before use, as it may not depending on how PHP was built.
+#798526 by jasonn1234: expand server-side control over modal js settings to include background opacity and color.
+#742832 by c960657: cache file system scans in ctools_plugin_load_includes().
+#758750 by mgriego, snufkin: Fix extra whitespace being added to exported scalars.
+#789598 by c960657: Pass block titles through check_plain().
+#780734 by c960657: Clean up node_comment_form plugin's form_comment_form_alter() implementation.
+Fix user_view task to use variable instead of value on hook invocation to avoid warnings on PHP5.3
+#771132: Move object cache system over to ctools_static() and introduce static var resets when locks are cleared.
+#762996 by jonskulski: Allow views_content panes to selectively show/hide their view's fields.
+#789524 by c960657: Implement "content type" hook on block content_type plugin.
+#782070: Relationship form submits were being passed an invalid part of $form_state.
+#767952 by aosodoev: Fix ctools_css_assemble() burping on multiple selectors by switching in str_replace() for preg_replace().
+#489256 by dww: Add a content_type plugin for outputting taxonomy terms from a node context.
+Assorted XSS, XSRF, and information disclosure security fixes.
+
+ctools 6.x-1.3 (2010-Feb-01)
+==============
 Added support for context keyword substitutions on override paths in content panes provided by views_content.
 #612850: Fix crash bug with panes referencing deleted/missing views.
 #595442: Fix AJAX problems with mod_security enabled.
@@ -57,14 +222,28 @@ Change plugins to no longer need magic function. Can now use $plugin = array(...
 Allow "admin path" to be empty for tasks to support Panels Everywhere.
 Introduce 'export callback' to individual fields in export.inc. Add some documentation to export.html -- much of it borrowed from stella!
 #686826 by dagmar: Improve AJAX error notifications.
+#625696 by dmmckenna: Bulk exporter did not define empty array at beginning in code it creates for you.
+#651852 by coreyp_1: Add caching to content subtypes to save some queries. This means developers will need to clear caches when changing content type code.
+#630982 by Roi Danton: Dependent javascript failed to work properly if radios being depended on had no value at all.
+#661332 by yhahn: Allow export.inc to export stdClass objects by exporting them as an array and casting them to an object.
+#654218 by Roi Danton: Improve documentation of dependent.inc to talk about annoyances with checkboxes, radios and fieldsets.
+#538092: Add converters to the node edit form context so node fields can be available.
+#484340: Support for token module in the context keywords.
+#662242: Page Manager was not removing menu items when pages were deleted due to caching.
+#639548: export.inc cache was not properly respected when loading individual items, leading to multiple redundant queries.
+#573646: Attempt to make sure page manager does not try to respond to menu/theme hooks when CTools is not enabled.
+#544438: Extend user context to allow selection of user, including "logged in user".
+#604976: Prevent Page Manager from throwing errors when no modules that provide variants are available.
+Add 'Update and save' button to reduce the number of needed steps to do basic content editing.
+#693742: Sanitize block admin output (to Panels drag & drop for example) to get rid of script tags.
 
-ctools 6.x-1.2
+ctools 6.x-1.2 (2009-Oct-21)
 ==============
 #605990 by johnskulski: Prevent Views PHP error if display was removed.
 #605968 by johnskulski: Show more information in the collapsible for Views Panes.
 #609024: Fix improper validation of views introduced in #547686.
 
-ctools 6.x-1.1
+ctools 6.x-1.1 (2009-Oct-15)
 ==============
 
 Fix problem with ctools_set_page_token()
@@ -108,7 +287,7 @@ Allow override of poll page.
 #599428: Enable and Disable variant buttons led to "Operation trail does not exist."
 Show a LOCK icon on the page list when pages are locked for editing either by the current user or another user.
 
-ctools 6.x-1.0
+ctools 6.x-1.0 (2009-Aug-19)
 ==============
 
 #534570: Fix _ and - not recognized in for context keywords.
index 928622d..fdd7c55 100644 (file)
@@ -56,10 +56,14 @@ function bulk_export_export() {
   $exportables = $export_tables = array();
 
   foreach ($schemas as $table => $schema) {
-    if (function_exists($schema['export']['list callback'])) {
+    if (!empty($schema['export']['list callback']) && function_exists($schema['export']['list callback'])) {
       $exportables[$table] = $schema['export']['list callback']();
-      $export_tables[$table] = $schema['module'];
     }
+    else {
+      $exportables[$table] = ctools_export_default_list($table, $schema);
+    }
+    natcasesort($exportables[$table]);
+    $export_tables[$table] = $schema['module'];
   }
   if ($exportables) {
     $form_state = array(
@@ -219,6 +223,7 @@ function bulk_export_export_form_submit($form, &$form_state) {
   foreach ($form_state['values']['tables'] as $table => $names) {
     $names = array_keys(array_filter($names));
     if ($names) {
+      natcasesort($names);
       ctools_export_to_hook_code($code, $table, $names, $name);
     }
   }
index 3f2b074..768406f 100644 (file)
@@ -13,6 +13,7 @@
 
 a.ctools-ajaxing,
 input.ctools-ajaxing,
+button.ctools-ajaxing,
 select.ctools-ajaxing {
   padding-right: 18px !important;
   background: url(../images/status-active.gif) right center no-repeat;
index 4453191..294df6c 100644 (file)
@@ -30,8 +30,7 @@ div.ctools-modal-content .modal-header a {
 div.ctools-modal-content .modal-content {
   padding: 1em 1em 0 1em;
   overflow: auto;
-  width: 575px;
-  height: 400px;
+  position: relative; /* Keeps IE7 from flowing outside the modal. */
 }
 
 div.ctools-modal-content .modal-form {
@@ -101,4 +100,7 @@ div.ctools-modal-content .container-inline .form-item {
   margin-right: 2em;
 }
 
-
+#views-exposed-pane-wrapper .form-item {
+  margin-top: 0;
+  margin-bottom: 0;
+}
index 80dbe36..a4686b8 100644 (file)
@@ -8,7 +8,7 @@
 
 /**
  * Use requirements to ensure that the CTools CSS cache directory can be
- * created.
+ * created and that the PHP version requirement is met.
  */
 function ctools_requirements($phase) {
   $requirements = array();
@@ -25,6 +25,13 @@ function ctools_requirements($phase) {
       $requirements['ctools_css_cache']['severity'] = REQUIREMENT_ERROR;
       $requirements['ctools_css_cache']['value'] = t('Unable to create');
     }
+
+    if (!function_exists('error_get_last')) {
+         $requirements['ctools_php_52']['title'] = t('CTools PHP requirements');
+      $requirements['ctools_php_52']['description'] = t('CTools requires certain features only available in PHP 5.2.0 or higher.');
+      $requirements['ctools_php_52']['severity'] = REQUIREMENT_WARNING;
+      $requirements['ctools_php_52']['value'] = t('PHP !version', array('!version' => phpversion()));
+    }
   }
 
   return $requirements;
@@ -34,8 +41,6 @@ function ctools_requirements($phase) {
  * Implements hook_schemea
  */
 function ctools_schema() {
-  // Currently, schema 1 is the only schema we have. As we make updates,
-  // we might either create a new schema 2 or make adjustments here.
   return ctools_schema_2();
 }
 
@@ -168,3 +173,26 @@ function ctools_update_6004() {
   db_add_primary_key('ctools_object_cache', array('sid', 'obj', 'name'));
   db_drop_index('ctools_object_cache', 'sid_obj_name');
 }
+
+/**
+ * Removed update.
+ */
+function ctools_update_6005() {
+  return array();
+}
+
+/**
+ * ctools_custom_content table was originally here, but is now moved to
+ * its own module.
+ */
+function ctools_update_6007() {
+  $ret = array();
+  if (db_table_exists('ctools_custom_content')) {
+    // Enable the module to make everything as seamless as possible.
+    drupal_install_modules(array('ctools_custom_content'));
+  }
+
+  return $ret;
+}
+
+
index 5916b35..c089a46 100644 (file)
@@ -10,7 +10,7 @@
  * must be implemented in the module file.
  */
 
-define('CTOOLS_API_VERSION', '2.0.0');
+define('CTOOLS_API_VERSION', '1.8');
 
 /**
  * Test the CTools API version.
@@ -75,6 +75,9 @@ function ctools_api_version($minimum, $maximum = NULL) {
   return TRUE;
 }
 
+// -----------------------------------------------------------------------
+// General utility functions
+
 /**
  * Include .inc files as necessary.
  *
@@ -106,72 +109,59 @@ function ctools_include($file, $module = 'ctools', $dir = 'includes') {
   static $used = array();
   if (!isset($used[$module][$dir][$file])) {
     require_once DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$dir/$file.inc";
+    $used[$module][$dir][$file] = true;
   }
-
-  $used[$file] = TRUE;
 }
 
 /**
- * Provide the proper path to a CTools image
+ * Provide the proper path to an image as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $image
+ *   The base file name (with extension)  of the image to be included.
+ * @param $module
+ *   Optional module containing the include.
+ * @param $dir
+ *   Optional subdirectory containing the include file.
  */
-function ctools_image_path($image) {
-  return drupal_get_path('module', 'ctools') . '/images/' . $image;
+function ctools_image_path($image, $module = 'ctools', $dir = 'images') {
+  return drupal_get_path('module', $module) . "/$dir/" . $image;
 }
 
 /**
- * Include views .css files.
+ * Include css files as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $file
+ *   The base file name to be included.
+ * @param $module
+ *   Optional module containing the include.
+ * @param $dir
+ *   Optional subdirectory containing the include file.
  */
-function ctools_add_css($file) {
-  drupal_add_css(drupal_get_path('module', 'ctools') . "/css/$file.css");
+function ctools_add_css($file, $module = 'ctools', $dir = 'css') {
+  drupal_add_css(drupal_get_path('module', $module) . "/$dir/$file.css");
 }
 
 /**
- * Include views .js files.
+ * Include js files as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $file
+ *   The base file name to be included.
+ * @param $module
+ *   Optional module containing the include.
+ * @param $dir
+ *   Optional subdirectory containing the include file.
  */
-function ctools_add_js($file) {
-  drupal_add_js(drupal_get_path('module', 'ctools') . "/js/$file.js");
-}
-
-/**
- * Implement hook_init to keep our global CSS at the ready.
- */
-function ctools_init() {
-  ctools_add_css('ctools');
-  // If we are sure that CTools' AJAX is in use, change the error handling.
-  if (!empty($_REQUEST['ctools_ajax'])) {
-    ini_set('display_errors', 0);
-    register_shutdown_function('ctools_shutdown_handler');
-  }
-}
-
-/**
- * Shutdown handler used during ajax operations to help catch fatal errors.
- */
-function ctools_shutdown_handler() {
-  if ($error = error_get_last()){
-    switch($error['type']){
-      case E_ERROR:
-      case E_CORE_ERROR:
-      case E_COMPILE_ERROR:
-      case E_USER_ERROR:
-        // Do this manually because including files here is dangerous.
-        $commands = array(
-          array(
-            'command' => 'alert',
-            'title' => t('Error'),
-            'text' => t('Unable to complete operation. Fatal error in @file on line @line: @message', array(
-              '@file' => $error['file'],
-              '@line' => $error['line'],
-              '@message' => $error['message'],
-            )),
-          ),
-        );
-
-        // Change the status code so that the client will read the AJAX returned.
-        header('HTTP/1.1 200 OK');
-        drupal_json($commands);
-    }
-  }
+function ctools_add_js($file, $module = 'ctools', $dir = 'js') {
+  drupal_add_js(drupal_get_path('module', $module) . "/$dir/$file.js");
 }
 
 /**
@@ -225,79 +215,169 @@ function ctools_static_reset($name) {
 }
 
 /**
- * Provide a hook passthrough to included files.
+ * Get a list of roles in the system.
  *
- * To organize things neatly, each CTools tool gets its own toolname.$type.inc
- * file. If it exists, it's loaded and ctools_$tool_$type() is executed.
- * To save time we pass the $items array in so we don't need to do array
- * addition. It modifies the array by reference and doesn't need to return it.
+ * @return
+ *   An array of role names keyed by role ID.
  */
-function _ctools_passthrough(&$items, $type = 'theme') {
-  $files = drupal_system_listing('/\.' . $type . '\.inc$/', drupal_get_path('module', 'ctools') . '/includes', 'name', 0);
-  foreach ($files as $file) {
-    require_once DRUPAL_ROOT . '/' . $file->uri;
-    list($tool) = explode('.', $file->name, 2);
-
-    $function = 'ctools_' . $tool . '_' . $type;
-    if (function_exists($function)) {
-      $function($items);
+function ctools_get_roles() {
+  static $roles = NULL;
+  if (!isset($roles)) {
+    $roles = array();
+    $result = db_query("SELECT r.rid, r.name FROM {role} r ORDER BY r.name");
+    while ($obj = db_fetch_object($result)) {
+      $roles[$obj->rid] = $obj->name;
     }
   }
+
+  return $roles;
 }
 
-/**
- * Implements hook_theme_registry_alter()
+/*
+ * Break x,y,z and x+y+z into an array. Numeric only.
+ *
+ * @param $str
+ *   The string to parse.
+ *
+ * @return $object
+ *   An object containing
+ *   - operator: Either 'and' or 'or'
+ *   - value: An array of numeric values.
  */
-function ctools_theme_registry_alter(&$registry) {
-  if ($registry['menu_local_tasks']['function'] == 'theme_menu_local_tasks') {
-    $registry['menu_local_tasks'] = array(
-      'function' => 'ctools_theme_menu_local_tasks',
-      'path' => drupal_get_path('module', 'ctools') . '/includes',
-      'file' => 'menu.inc',
-    ) + $registry['menu_local_tasks'];
+function ctools_break_phrase($str) {
+  $object = new stdClass();
+
+  if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) {
+    // The '+' character in a query string may be parsed as ' '.
+    $object->operator = 'or';
+    $object->value = preg_split('/[+ ]/', $str);
+  }
+  else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) {
+    $object->operator = 'and';
+    $object->value = explode(',', $str);
   }
 
-  if (isset($registry['help']['function']) && $registry['help']['function'] == 'theme_help') {
-    $registry['help'] = array(
-      'function' => 'ctools_menu_help',
-      'path' => drupal_get_path('module', 'ctools') . '/includes',
-      'file' => 'menu.inc',
-    ) + $registry['help'];
+  // Keep an 'error' value if invalid strings were given.
+  if (!empty($str) && (empty($object->value) || !is_array($object->value))) {
+    $object->value = array(-1);
+    $object->invalid_input = TRUE;
+    return $object;
   }
 
-/*
-  // Handle a special override for garland because it's cute and does its own
-  // thing with tabs and we can't ask users to edit a core theme for us.
-  if ($registry['menu_local_tasks']['function'] == 'phptemplate_menu_local_tasks' &&
-      $registry['menu_local_tasks']['theme paths'][1] == 'themes/garland') {
-    $registry['menu_local_tasks'] = array(
-      'function' => 'ctools_garland_menu_local_tasks',
-      'path' => drupal_get_path('module', 'ctools') . '/includes',
-      'file' => 'menu.inc',
-    ) + $registry['menu_local_tasks'];
+  if (empty($object->value)) {
+    $object->value = array();
   }
 
-  if (isset($registry['page']['preprocess functions'][2]) &&
-      $registry['page']['preprocess functions'][2] == 'phptemplate_preprocess_page' &&
-      $registry['page']['theme paths'][1] == 'themes/garland') {
-    $registry['page']['preprocess functions'][2] = 'ctools_garland_preprocess_page';
+  // Doubly ensure that all values are numeric only.
+  foreach ($object->value as $id => $value) {
+    $object->value[$id] = intval($value);
+  }
+
+  return $object;
+}
+
+/**
+ * Set a token/value pair to be replaced later in the request, specifically in
+ * ctools_preprocess_page().
+ *
+ * @param $token
+ *   The token to be replaced later, during page rendering.  This should
+ *    ideally be a string inside of an HTML comment, so that if there is
+ *    no replacement, the token will not render on the page.
+ * @param $type
+ *   The type of the token. Can be either 'variable', which will pull data
+ *   directly from the page variables
+ * @param $argument
+ *   If $type == 'variable' then argument should be the key to fetch from
+ *   the $variables. If $type == 'callback' then it should either be the
+ *   callback, or an array that will be sent to call_user_func_array().
+ *
+ * @return
+ *   A array of token/variable names to be replaced.
+ */
+function ctools_set_page_token($token = NULL, $type = NULL, $argument = NULL) {
+  static $tokens = array();
+
+  if (isset($token)) {
+    $tokens[$token] = array($type, $argument);
   }
-*/
+  return $tokens;
 }
 
+/**
+ * Easily set a token from the page variables.
+ *
+ * This function can be used like this:
+ * $token = ctools_set_variable_token('tabs');
+ *
+ * $token will then be a simple replacement for the 'tabs' about of the
+ * variables available in the page template.
+ */
+function ctools_set_variable_token($token) {
+  $string = '<!-- ctools-page-' . $token . ' -->';
+  ctools_set_page_token($string, 'variable', $token);
+  return $string;
+}
 
 /**
- * Override or insert PHPTemplate variables into the templates.
+ * Easily set a token from the page variables.
  *
- * This needs to be in the .module file to ensure availability; we can't change the
- * paths or it won't be able to find templates.
+ * This function can be used like this:
+ * $token = ctools_set_variable_token('id', 'mymodule_myfunction');
  */
-function ctools_garland_preprocess_page(&$vars) {
-  $vars['tabs2'] = ctools_menu_secondary_local_tasks();
+function ctools_set_callback_token($token, $callback) {
+  $string = '<!-- ctools-page-' . $token . ' -->';
+  ctools_set_page_token($string, 'callback', $callback);
+  return $string;
+}
 
-  // Hook into color.module
-  if (module_exists('color')) {
-    _color_page_alter($vars);
+// -----------------------------------------------------------------------
+// Drupal core hooks
+
+/**
+ * Implement hook_init to keep our global CSS at the ready.
+ */
+function ctools_init() {
+  ctools_add_css('ctools');
+  // If we are sure that CTools' AJAX is in use, change the error handling.
+  if (!empty($_REQUEST['ctools_ajax'])) {
+    ini_set('display_errors', 0);
+    register_shutdown_function('ctools_shutdown_handler');
+  }
+
+  // Clear plugin cache on the module page submit.
+  if ($_GET['q'] == 'admin/build/modules/list/confirm' && !empty($_POST)) {
+    cache_clear_all('ctools_plugin_files:', 'cache', TRUE);
+  }
+}
+
+/**
+ * Shutdown handler used during ajax operations to help catch fatal errors.
+ */
+function ctools_shutdown_handler() {
+  if (function_exists('error_get_last') AND ($error = error_get_last())){
+    switch($error['type']){
+      case E_ERROR:
+      case E_CORE_ERROR:
+      case E_COMPILE_ERROR:
+      case E_USER_ERROR:
+        // Do this manually because including files here is dangerous.
+        $commands = array(
+          array(
+            'command' => 'alert',
+            'title' => t('Error'),
+            'text' => t('Unable to complete operation. Fatal error in @file on line @line: @message', array(
+              '@file' => $error['file'],
+              '@line' => $error['line'],
+              '@message' => $error['message'],
+            )),
+          ),
+        );
+
+        // Change the status code so that the client will read the AJAX returned.
+        header('HTTP/1.1 200 OK');
+        drupal_json($commands);
+    }
   }
 }
 
@@ -305,6 +385,7 @@ function ctools_garland_preprocess_page(&$vars) {
  * Implements hook_theme().
  */
 function ctools_theme() {
+  ctools_include('utility');
   $items = array();
   _ctools_passthrough($items, 'theme');
   return $items;
@@ -314,33 +395,178 @@ function ctools_theme() {
  * Implements hook_menu().
  */
 function ctools_menu() {
+  ctools_include('utility');
   $items = array();
   _ctools_passthrough($items, 'menu');
   return $items;
 }
 
 /**
- * Implements hook_ctools_plugin_dierctory() to let the system know
- * we implement task and task_handler plugins.
+ * Implementation of hook_cron. Clean up old caches.
+ */
+function ctools_cron() {
+  ctools_include('utility');
+  _ctools_passthrough($items, 'cron');
+}
+
+/**
+ * Ensure the CTools CSS cache is flushed whenever hook_flush_caches is invoked.
  */
-function ctools_ctools_plugin_directory($module, $plugin) {
-  if ($module == 'ctools') {
-    return 'plugins/' . $plugin;
+function ctools_flush_caches() {
+  // Do not actually flush caches if running on cron. Drupal uses this hook
+  // in an inconsistent fashion and it does not necessarily mean to *flush*
+  // caches when running from cron. Instead it's just getting a list of cache
+  // tables and may not do any flushing.
+  if (variable_get('cron_semaphore', FALSE)) {
+    return;
   }
+
+  ctools_include('css');
+  ctools_css_flush_caches();
 }
 
 /**
- * Get a list of roles in the system.
+ * Provide a search form with a different id so that form_alters will miss it
+ * and thus not get advanced search settings.
  */
-function ctools_get_roles() {
-  static $roles = NULL;
-  if (!isset($roles)) {
-    $roles = db_query('SELECT r.rid, r.name FROM {role} r ORDER BY r.name')->fetchAllKeyed();
+function ctools_forms() {
+  $forms['ctools_search_form']= array(
+    'callback' => 'search_form',
+  );
+
+  return $forms;
+}
+
+/**
+ * Implementation of hook_file_download()
+ *
+ * When using the private file system, we have to let Drupal know it's ok to
+ * download CSS and image files from our temporary directory.
+ */
+function ctools_file_download($filepath) {
+  if (strpos($filepath, 'ctools') === 0) {
+    $mime = file_get_mimetype($filepath);
+    // For safety's sake, we allow only text and images.
+    if (strpos($mime, 'text') === 0 || strpos($mime, 'image') === 0) {
+      return array('Content-type:' . $mime);
+    }
   }
-  return $roles;
+}
+
+// -----------------------------------------------------------------------
+// CTools hook implementations.
+
+/**
+ * Implementation of hook_ctools_plugin_directory() to let the system know
+ * where all our own plugins are.
+ */
+function ctools_ctools_plugin_directory($owner, $plugin_type) {
+  if ($owner == 'ctools') {
+    return 'plugins/' . $plugin_type;
+  }
+}
+
+/**
+ * Implementation of hook_js_replacements().
+ * This is a hook that is not a standard yet. We hope jquery_update and others
+ * will expose this hook to inform modules which scripts they are modifying
+ * in the theme layer.
+ * The return format is $scripts[$type][$old_path] = $new_path.
+ */
+function ctools_js_replacements() {
+  $replacements = array();
+  // Until jquery_update is released with its own replacement hook, we will
+  // take those replacements into account here.
+  if (module_exists('jquery_update')) {
+    $replacements = array_merge_recursive($replacements, jquery_update_get_replacements());
+    foreach ($replacements as $type => $type_replacements) {
+      foreach ($type_replacements as $old_path => $new_filename) {
+        $replacements[$type][$old_path] = JQUERY_UPDATE_REPLACE_PATH . "/$new_filename";
+      }
+    }
+    $replacements['core']['misc/jquery.js'] = jquery_update_jquery_path();
+  }
+  return $replacements;
 }
 
 /**
+ * Inform CTools that the layout plugin can be loaded from themes.
+ */
+function ctools_ctools_plugin_access() {
+  return array(
+    'child plugins' => TRUE,
+  );
+}
+
+// -----------------------------------------------------------------------
+// Drupal theme preprocess hooks that must be in the .module file.
+
+/**
+ * Override or insert PHPTemplate variables into the templates.
+ *
+ * This needs to be in the .module file to ensure availability; we can't change the
+ * paths or it won't be able to find templates.
+ */
+function ctools_garland_preprocess_page(&$vars) {
+  $vars['tabs2'] = ctools_menu_secondary_local_tasks();
+
+  // Hook into color.module
+  if (module_exists('color')) {
+    _color_page_alter($vars);
+  }
+}
+
+/**
+ * A theme preprocess function to automatically allow panels-based node
+ * templates based upon input when the panel was configured.
+ */
+function ctools_preprocess_node(&$vars) {
+  // The 'panel_identifier' attribute of the node is added when the pane is
+  // rendered.
+  if (!empty($vars['node']->panel_identifier)) {
+    $vars['panel_identifier'] = check_plain($vars['node']->panel_identifier);
+    $vars['template_files'][] = 'node-panel-' . check_plain($vars['node']->panel_identifier);
+  }
+}
+
+/**
+ * A theme preprocess function to allow content type plugins to use page
+ * template variables which are not yet available when the content type is
+ * rendered.
+ */
+function ctools_preprocess_page(&$variables) {
+  $tokens = ctools_set_page_token();
+  if (!empty($tokens)) {
+    foreach ($tokens as $token => $key) {
+      list($type, $argument) = $key;
+      switch ($type) {
+        case 'variable':
+          $tokens[$token] = isset($variables[$argument]) ? $variables[$argument] : '';
+          break;
+        case 'callback':
+          if (is_string($argument) && function_exists($argument)) {
+            $tokens[$token] = $argument($variables);
+          }
+          if (is_array($argument) && function_exists($argument[0])) {
+            $function = array_shift($argument);
+            $argument = array_merge(array(&$variables), $argument);
+            $tokens[$token] = call_user_func_array($function, $argument);
+          }
+          break;
+      }
+    }
+    $variables['content'] = strtr($variables['content'], $tokens);
+  }
+
+  if (defined('CTOOLS_AJAX_INCLUDED')) {
+    ctools_ajax_page_preprocess($variables);
+  }
+}
+
+// -----------------------------------------------------------------------
+// Menu callbacks that must be in the .module file.
+
+/**
  * Determine if the current user has access via a plugin.
  *
  * This function is meant to be embedded in the Drupal menu system, and
@@ -381,80 +607,28 @@ function ctools_access_menu($access) {
 }
 
 /**
- * Implements hook_cron. Clean up old caches.
- */
-function ctools_cron() {
-  // TODO: Should this use the passthrough?
-  if (variable_get('ctools_last_cron', 0) < REQUEST_TIME - 86400) {
-    variable_set('ctools_last_cron', REQUEST_TIME);
-    ctools_include('object-cache');
-    ctools_object_cache_clean();
-  }
-}
-
-/*
- * Break x,y,z and x+y+z into an array. Numeric only.
+ * Determine if the current user has access via checks to multiple different
+ * permissions.
  *
- * @param $str
- *   The string to parse.
+ * This function is a thin wrapper around user_access that allows multiple
+ * permissions to be easily designated for use on, for example, a menu callback.
  *
- * @return $object
- *   An object containing
- *   - operator: Either 'and' or 'or'
- *   - value: An array of numeric values.
- */
-function ctools_break_phrase($str) {
-  $object = new stdClass();
-
-  if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) {
-    // The '+' character in a query string may be parsed as ' '.
-    $object->operator = 'or';
-    $object->value = preg_split('/[+ ]/', $str);
-  }
-  else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) {
-    $object->operator = 'and';
-    $object->value = explode(',', $str);
-  }
-
-  // Keep an 'error' value if invalid strings were given.
-  if (!empty($str) && (empty($object->value) || !is_array($object->value))) {
-    $object->value = array(-1);
-    $object->invalid_input = TRUE;
-    return $object;
-  }
-
-  if (empty($object->value)) {
-    $object->value = array();
-  }
-
-  // Doubly ensure that all values are numeric only.
-  foreach ($object->value as $id => $value) {
-    $object->value[$id] = intval($value);
-  }
-
-  return $object;
-}
-
-/**
- * A theme preprocess function to automatically allow panels-based node
- * templates based upon input when the panel was configured.
+ * @param ...
+ *   An indexed array of zero or more permission strings to be checked by
+ *   user_access().
+ *
+ * @return
+ *   Iff all checks pass will this function return TRUE. If an invalid argument
+ *   is passed (e.g., not a string), this function errs on the safe said and
+ *   returns FALSE.
  */
-function ctools_preprocess_node(&$vars) {
-  // The 'panel_identifier' attribute of the node is added when the pane is
-  // rendered.
-  if (!empty($vars['node']->panel_identifier)) {
-    $vars['panel_identifier'] = check_plain($vars['node']->panel_identifier);
-    $vars['template_files'][] = 'node-panel-' . check_plain($vars['node']->panel_identifier);
+function ctools_access_multiperm() {
+  foreach (func_get_args() as $arg) {
+    if (!is_string($arg) || !user_access($arg)) {
+      return FALSE;
+    }
   }
-}
-
-/**
- * Ensure the CTools CSS cache is flushed whenever hook_flush_caches is invoked.
- */
-function ctools_flush_caches() {
-  // TODO: Should this use the passthrough mechanism?
-  ctools_include('css');
-  ctools_css_flush_caches();
+  return TRUE;
 }
 
 /**
@@ -473,97 +647,122 @@ function ctools_js_load($js) {
 }
 
 /**
- * A theme preprocess function to allow content type plugins to use page
- * template variables which are not yet available when the content type is
- * rendered.
+ * Menu _load hook.
+ *
+ * This function will be called to load an object as a replacement for
+ * %ctools_export_ui in menu paths.
  */
-function ctools_preprocess_page(&$variables) {
-  $tokens = ctools_set_page_token();
-  if (!empty($tokens)) {
-    foreach ($tokens as $token => $key) {
-      list($type, $argument) = $key;
-      switch ($type) {
-        case 'variable':
-          $tokens[$token] = isset($variables[$argument]) ? $variables[$argument] : '';
-          break;
-        case 'callback':
-          if (is_string($argument) && function_exists($argument)) {
-            $tokens[$token] = $argument();
-          }
-          if (is_array($argument) && function_exists($argument[0])) {
-            $tokens[$token] = call_user_func_array($argument);
-          }
-          break;
-      }
+function ctools_export_ui_load($item_name, $plugin_name) {
+  $return = &ctools_static(__FUNCTION__, FALSE);
+
+  if (!$return) {
+    ctools_include('export-ui');
+    $plugin = ctools_get_export_ui($plugin_name);
+
+    if ($plugin) {
+      // Get the load callback.
+      $item = ctools_export_crud_load($plugin['schema'], $item_name);
+      return empty($item) ? FALSE : $item;
     }
-    $variables['content'] = strtr($variables['content'], $tokens);
   }
+
+  return $return;
 }
 
+// -----------------------------------------------------------------------
+// Caching callbacks on behalf of export-ui.
+
 /**
- * Set a token/value pair to be replaced later in the request, specifically in
- * ctools_preprocess_page().
- *
- * @param $token
- *   The token to be replaced later, during page rendering.  This should
- *    ideally be a string inside of an HTML comment, so that if there is
- *    no replacement, the token will not render on the page.
- * @param $type
- *   The type of the token. Can be either 'variable', which will pull data
- *   directly from the page variables
- * @param $argument
- *   If $type == 'variable' then argument should be the key to fetch from
- *   the $variables. If $type == 'callback' then it should either be the
- *   callback, or an array that will be sent to call_user_func_array().
- *
- * @return
- *   A array of token/variable names to be replaced.
+ * Menu access callback for various tasks of export-ui.
  */
-function ctools_set_page_token($token = NULL, $type = NULL, $argument = NULL) {
-  static $tokens = array();
+function ctools_export_ui_task_access($plugin_name, $op, $item = NULL) {
+  ctools_include('export-ui');
+  $plugin = ctools_get_export_ui($plugin_name);
+  $handler = ctools_export_ui_get_handler($plugin);
 
-  if (isset($token)) {
-    $tokens[$token] = array($type, $argument);
+  if ($handler) {
+    return $handler->access($op, $item);
   }
 
-  return $tokens;
+  // Deny access if the handler cannot be found.
+  return FALSE;
 }
 
 /**
- * Easily set a token from the page variables.
- *
- * This function can be used like this:
- * $token = ctools_set_variable_token('tabs');
- *
- * $token will then be a simple replacement for the 'tabs' about of the
- * variables available in the page template.
+ * Cache callback on behalf of ctools_export_ui.
  */
-function ctools_set_variable_token($token) {
-  $string = '<!-- ctools-page-' . $token . ' -->';
-  ctools_set_page_token($string, 'variable', $token);
-  return $string;
+function ctools_export_ui_context_cache_get($plugin_name, $key) {
+  ctools_include('export-ui');
+  $plugin = ctools_get_export_ui($plugin_name);
+  $handler = ctools_export_ui_get_handler($plugin);
+  if ($handler) {
+    $item = $handler->edit_cache_get($key);
+    if (!$item) {
+      $item = ctools_export_crud_load($handler->plugin['schema'], $key);
+    }
+    return $item;
+  }
 }
 
 /**
- * Easily set a token from the page variables.
- *
- * This function can be used like this:
- * $token = ctools_set_variable_token('id', 'mymodule_myfunction');
+ * Cache callback on behalf of ctools_export_ui.
  */
-function ctools_set_callback_token($token, $callback) {
-  $string = '<!-- ctools-page-' . $token . ' -->';
-  ctools_set_page_token($string, 'callback', $callback);
-  return $string;
+function ctools_export_ui_context_cache_set($plugin_name, $key, $item) {
+  ctools_include('export-ui');
+  $plugin = ctools_get_export_ui($plugin_name);
+  $handler = ctools_export_ui_get_handler($plugin);
+  if ($handler) {
+    return $handler->edit_cache_set_key($item, $key);
+  }
 }
 
 /**
- * Provide a search form with a different id so that form_alters will miss it
- * and thus not get advanced search settings.
+ * Callback for access control ajax form on behalf of export ui.
+ *
+ * Returns the cached access config and contexts used.
+ * Note that this is assuming that access will be in $item->access -- if it
+ * is not, an export UI plugin will have to make its own callbacks.
  */
-function ctools_forms() {
-  $forms['ctools_search_form']= array(
-    'callback' => 'search_form',
-  );
+function ctools_export_ui_ctools_access_get($argument) {
+  ctools_include('export-ui');
+  list($plugin_name, $key) = explode(':', $argument);
+
+  $plugin = ctools_get_export_ui($plugin_name);
+  $handler = ctools_export_ui_get_handler($plugin);
+
+  if ($handler) {
+    ctools_include('context');
+    $item = $handler->edit_cache_get($key);
+    if (!$item) {
+      $item = ctools_export_crud_load($handler->plugin['schema'], $key);
+    }
 
-  return $forms;
+    $contexts = ctools_context_load_contexts($item);
+    return array($item->access, $contexts);
+  }
+}
+
+/**
+ * Callback for access control ajax form on behalf of export ui
+ *
+ * Returns the cached access config and contexts used.
+ * Note that this is assuming that access will be in $item->access -- if it
+ * is not, an export UI plugin will have to make its own callbacks.
+ */
+function ctools_export_ui_ctools_access_set($argument, $access) {
+  ctools_include('export-ui');
+  list($plugin_name, $key) = explode(':', $argument);
+
+  $plugin = ctools_get_export_ui($plugin_name);
+  $handler = ctools_export_ui_get_handler($plugin);
+
+  if ($handler) {
+    ctools_include('context');
+    $item = $handler->edit_cache_get($key);
+    if (!$item) {
+      $item = ctools_export_crud_load($handler->plugin['schema'], $key);
+    }
+    $item->access = $access;
+    return $handler->edit_cache_set_key($item, $key);
+  }
 }
index 9ea3243..667b055 100644 (file)
@@ -16,7 +16,7 @@ $plugin = array(
   'description' => t('Control access by length of simplecontext argument.'),
   'callback' => 'ctools_plugin_example_arg_length_ctools_access_check',
   'settings form' => 'ctools_plugin_example_arg_length_ctools_access_settings',
-  'summary' => 'ctools_plugin_example_arg_length_ctools_acesss_summary',
+  'summary' => 'ctools_plugin_example_arg_length_ctools_access_summary',
   'required context' => new ctools_context_required(t('Simplecontext'), 'simplecontext'),
 );
 
@@ -58,7 +58,7 @@ function ctools_plugin_example_arg_length_ctools_access_check($conf, $context) {
 /**
  * Provide a summary description based upon the checked roles.
  */
-function ctools_plugin_example_arg_length_ctools_acesss_summary($conf, $context) {
+function ctools_plugin_example_arg_length_ctools_access_summary($conf, $context) {
   return t('Simpletext argument must be !comp @length characters',
     array('!comp' => $conf['greater_than'] ? 'greater than' : 'less than or equal to',
       '@length' => $conf['arg_length']));
index 9472c61..a9c8d71 100644 (file)
@@ -18,7 +18,7 @@ $plugin = array(
   'callback' => 'ctools_plugin_example_example_role_ctools_access_check',
   'default' => array('rids' => array()),
   'settings form' => 'ctools_plugin_example_example_role_ctools_access_settings',
-  'summary' => 'ctools_plugin_example_example_role_ctools_acesss_summary',
+  'summary' => 'ctools_plugin_example_example_role_ctools_access_summary',
   'required context' => new ctools_context_required(t('User'), 'user'),
 );
 
@@ -60,7 +60,7 @@ function ctools_plugin_example_example_role_ctools_access_check($conf, $context)
 /**
  * Provide a summary description based upon the checked roles.
  */
-function ctools_plugin_example_example_role_ctools_acesss_summary($conf, $context) {
+function ctools_plugin_example_example_role_ctools_access_summary($conf, $context) {
   if (!isset($conf['rids'])) {
     $conf['rids'] = array();
   }
index 0bd639b..28f4fbf 100644 (file)
@@ -40,9 +40,9 @@ title = Miscellaneous menu helper tool
 title = Plugins and APIs tool
 weight = -50
 
-; [plugins-api]
-; title = Implementing APIs
-; parent = plugins
+[plugins-api]
+title = Implementing APIs
+parent = plugins
 
 [plugins-creating]
 title = Creating plugins
@@ -55,6 +55,9 @@ parent = plugins
 [export]
 title = Exportable objects tool
 
+[export-ui]
+title = Exportable objects UI creator
+
 [form]
 title = Form tools
 
index 475ad55..49b66d8 100644 (file)
@@ -16,14 +16,18 @@ To make your objects exportable, you do have to do a medium amount of work.
 <li>Create an import and export mechanism from the UI.</li>
 </ol>
 <h3>The export section of the schema file</h3>
+
 Exportable objects are created by adding definition to the schema in an 'export' section. For example:
+
 <pre>
 function mymodule_schema() {
   $schema['mymodule_myobj'] = array(
     'description' => t('Table storing myobj definitions.'),
     'export' => array(
       'key' => 'name',
-      'identifier' => 'obj', // Exports will be as $myobj
+      'key name' => 'Name',
+      'primary key' => 'oid',
+      'identifier' => 'myobj', // Exports will be as $myobj
       'default hook' => 'default_mymodule_myobj',  // Function hook name.
       'api' => array(
         'owner' => 'mymodule',
@@ -31,7 +35,43 @@ function mymodule_schema() {
         'minimum_version' => 1,
         'current_version' => 1,
       ),
+      // If the key is stored in a table that is joined in, specify it:
+      'key in table' => 'my_join_table',
+
     ),
+
+    // If your object's data is split up across multiple tables, you can
+    // specify additional tables to join. This is very useful when working
+    // with modules like exportables.module that has a special table for
+    // translating keys to local database IDs.
+    //
+    // The joined table must have its own schema definition.
+    //
+    // If using joins, you should implement a 'delete callback' (see below)
+    // to ensure that deletes happen properly. export.inc does not do this
+    // automatically!
+    'join' => array(
+      'exportables' => array(
+        // The following parameters will be used in this way:
+        // SELECT ... FROM {mymodule_myobj} t__0 INNER JOIN {my_join_table} t__1 ON t__0.id = t__1.id AND extras
+        'table' => 'my_join_table',
+        'left_key' => 'format',
+        'right_key' => 'id',
+        // Optionally you can define extra clauses to add to the INNER JOIN
+        'extras' => "AND extra_clauses",
+
+        // You must specify which fields will be loaded. These fields must
+        // exist in the schema definition of the joined table.
+        'load' => array(
+          'machine',
+        ),
+
+        // And finally you can define other tables to perform INNER JOINS
+        //'other_joins' => array(
+        //   'table' => ...
+        //),
+      ),
+    )
     'fields' => array(
       'name' => array(
         'type' => 'varchar',
@@ -59,8 +99,14 @@ function mymodule_schema() {
 <dt>key</dt>
 <dd>This is the primary key of the exportable object and should be a string as names are more portable across systems. It is possible to use numbers here, but be aware that export collisions are very likely. Defaults to 'name'.</dd>
 
+<dt>key name</dt>
+<dd>Human readable title of the export key. Defaults to 'Name'. Because the schema is cached, do not translate this. It must instead be translated when used.</dd>
+
+<dt>primary key</dt>
+<dd>Objects should contain a primary key which is a database identifier primarily used to determine if an object has been written or not. This is required for the default CRUD save callback to work.</dd>
+
 <dt>object</dt>
-<dd>The class the object should be created ass. Defaults as stdClass.</dd>
+<dd>The class the object should be created as. Defaults as stdClass.</dd>
 
 <dt>can disable</dt>
 <dd>Control whether or not the exportable objects can be disabled. All this does is cause the 'disabled' field on the object to always be set appropriately, and a variable is kept to record the state. Changes made to this state must be handled by the owner of the object. Defaults to TRUE.</dd>
@@ -74,19 +120,37 @@ function mymodule_schema() {
 <dt>identifier</dt>
 <dd>When exporting the object, the identifier is the variable that the exported object will be placed in. Defaults to $table.</dd>
 
-<dt>bulk export'</dt>
-<dd>Declares whether or not the exportable will be available for bulk exporting. (Bulk export UI is currently left to be handled by contrib, though the core hooks are present so this can be done.).</dd>
-
-<dt>export callback</dt>
-<dd>The callback to use for bulk exporting. Defaults to $module . '_export_' . $table. This function will receive the $myobject and $indent as arguments.</dd>
+<dt>bulk export</dt>
+<dd>Declares whether or not the exportable will be available for bulk exporting.</dd>
 
 <dt>list callback</dt>
-<dd>Bulk export callback to provide a list of exportable objects to be chosen for bulk exporting. Defaults to $module . '_' . $table . '_list'.</dd>
+<dd>Bulk export callback to provide a list of exportable objects to be chosen for bulk exporting. Defaults to $module . '_' . $table . '_list' if the function exists. If it is not, a default listing function will be provided that will make a best effort to list the titles. See ctools_export_default_list().</dd>
 
 <dt>to hook code callback</dt>
 <dd>Function used to generate an export for the bulk export process. This is only necessary if the export is more complicated than simply listing the fields. Defaults to $module . '_' . $table . '_to_hook_code'.</dt>
 </dl>
 
+<dt>create callback</dt>
+<dd>CRUD callback to use to create a new exportable item in memory. If not provided, the default function will be used. The single argument is a boolean used to determine if defaults should be set on the object. This object will not be written to the database by this callback.</dd>
+
+<dt>load callback</dt>
+<dd>CRUD callback to use to load a single item. If not provided, the default load function will be used. The callback will accept a single argument which should be an identifier of the export key.</dd>
+
+<dt>load all callback</dt>
+<dd>CRUD callback to use to load all items, usually for administrative purposes. If not provided, the default load function will be used. The callback will accept a single argument to determine if the load cache should be reset or not.</dd>
+
+<dt>save callback</dt>
+<dd>CRUD callback to use to save a single item. If not provided, the default save function will be used. The callback will accept a single argument which should be the complete exportable object to save.</dd>
+
+<dt>delete callback</dt>
+<dd>CRUD callback to use to delete a single item. If not provided, the default delete function will be used. The callback will accept a single argument which can be *either* the object or just the export key to delete. The callback MUST be able to accept either.</dd>
+
+<dt>export callback</dt>
+<dd>CRUD callback to use for exporting. If not provided, the default export function will be used. The callback will accept two arguments, the first is the item to export, the second is the indent to place on the export, if any.</dd>
+
+<dt>import callback</dt>
+<dd>CRUD callback to use for importing. If not provided, the default export function will be used. This function will accept the code as a single argument and, if the code evaluates, return an object represented by that code. In the case of failure, this will return a string with human readable errors.</dd>
+
 In addition, each field can contain the following:
 <dl>
 <dt>no export</dt>
@@ -96,174 +160,69 @@ In addition, each field can contain the following:
 <dd>A function to override the export behavior. It will receive ($myobject, $field, $value, $indent) as arguments. By default, fields are exported through ctools_var_export().</dd>
 </dl>
 
-<h3>The load function</h3>
-Typically, there will be two load functions. A 'single' load, to load just one object, and an 'all' load, to load all of the objects for use in administrating the objects or utilizing the objects when you need all of them. Using ctools_export_load_object() you can easily do both, as well as quite a bit in between. This example shows loading just one object:
+<h3>Reserved keys on exportable objects</h3>
 
-<pre>
-/**
-* Load a single myobj.
-*/
-function mymodule_myobj_load($name) {
-  ctools_include('export');
-  $result = ctools_export_load_object('mymodule_myobjs', 'names', array($name));
-  if (isset($result[$name])) {
-    return $result[$name];
-  }
-}
-</pre>
+Exportable objects have several reserved keys that are used by the CTools export API. Each key can be found at <code>$myobj-&gt;{$key}</code> on an object loaded through <code>ctools_export_load_object()</code>. Implementing modules should not use these keys as they will be overwritten by the CTools export API.
+<dl>
+<dt>api_version</dt>
+<dd>The API version that this object implements.</dd>
 
-<h3>The save function</h3>
-The export mechanism does not actually provide a method to save an object. This is okay, because in the simple case, drupal_write_record() does the job well. In the more complex cases, it would not work for you anyway.
+<dt>disabled</dt>
+<dd>A boolean for whether the object is disabled.</dd>
 
-<pre>
-/**
-* Load a single myobj.
-*/
-function mymodule_myobj_save(&$myobj) {
-  $update = (isset($myobj->oid) && is_numeric($myobj->oid)) ? array('oid') : array();
-  drupal_write_record('myobj', $myobj, $update);
-}
-</pre>
+<dt>export_module</dt>
+<dd>For objects that live in code, the module which provides the default object.</dd>
 
-<h3>Export from the UI</h3>
-In your hook_menu() you should define a path where users can export your object object. In our example below, this is 'admin/build/mymodule/%/export' where the % matches our myobj's unique identifier. This identifier will be passed to a form called mymodule_export_myobj.
-<pre>
-/**
-* Implementation of hook_menu().
-*/
-function mymodule_menu() {
-  $items['admin/build/mymodule/%/export'] = array(
-    'title' => 'Export',
-    'page callback' => 'mymodule_export_myobj',
-    'page arguments' => array(3),
-    'access arguments' => array('mymodule export permission'),
-    'type' => MENU_CALLBACK,
-  );
+<dt>export_type</dt>
+<dd>A bitmask representation of an object current storage. You can use this bitmask in combination with the <code>EXPORT_IN_CODE</code> and <code>EXPORT_IN_DATABASE</code> constants to test for an object's storage in your code.
+</dd>
 
-  return $items;
-}
+<dt>in_code_only</dt>
+<dd>A boolean for whether the object lives only in code.</dd>
 
-/**
-* Export a myobj and display it in a form.
-*/
-function mymodule_export_myobj(&$form_state, $oid) {
-  $myobj = mymodule_myobj_load($oid);
-  drupal_set_title(check_plain($myobj->description));
-  $code = mymodule_myobj_export($myobj);
-  return drupal_get_form('ctools_export_form', $code, $check_plain($myobj->description));
-}
-</pre>
+<dt>table</dt>
+<dd>The schema API table that this object belongs to.</dd>
+
+<dt>type</dt>
+<dd>A string representing the storage type of this object. Can be one of the following:
+<ul>
+<li><em>Normal</em> is an object that lives only in the database.</li>
+<li><em>Overridden</em> is an object that lives in the database and is overriding the exported configuration of a corresponding object in code.</li>
+<li><em>Default</em> is an object that lives only in code.</li>
+</ul>
+</dd>
+</dl>
+
+<h3>The load callback</h3>
+Calling ctools_export_crud_load($table, $name) will invoke your load callback, and calling ctools_export_crud_load_all($table, $reset) will invoke your load all callback. The default handlers should be sufficient for most uses.
 
-The mymodule_export_myobj() form function calls two functions, mymodule_myobj_load() to fetch the object in the desired format, and mymodule_myobj_export() to generate the exportable.
+Typically, there will be two load functions. A 'single' load, to load just one object, and an 'all' load, to load all of the objects for use in administrating the objects or utilizing the objects when you need all of them. Using ctools_export_load_object() you can easily do both, as well as quite a bit in between. This example duplicates the default functionality for loading one myobj.
 
 <pre>
 /**
 * Load a single myobj.
 */
-function mymodule_myobj_load($oid) {
+function mymodule_myobj_load($name) {
   ctools_include('export');
-  $result = ctools_export_load_object('mymodule_myobjs', 'names', array($oid));
-  if (isset($result[$oid])) {
-    return $result[$oid];
+  $result = ctools_export_load_object('mymodule_myobjs', 'names', array($name));
+  if (isset($result[$name])) {
+    return $result[$name];
   }
 }
-
-/**
-* Export a myobj.
-*
-* By declaring this function in the schema, anyone can easily export a myobj
-* just by knowing that myobj exists.
-*/
-function mymodule_myobj_export($myobj, $indent = '') {
-  ctools_include('export');
-  $output = ctools_export_object('mymodule_myobjs', $myobj, $indent);
-  return $output;
-}
 </pre>
 
-<h3>Import from the UI</h3>
-Please note: Importing objects means you let the user run PHP code. Do not give this permission to untrusted users.
-
-Much like with exporting, you'll need a menu entry to handle the import:
+<h3>The save callback</h3>
+Calling ctools_export_crud_save($table, $object) will invoke your save callback. The default handlers should be sufficient for most uses. For the default save mechanism to work, you <b>must</b> define a 'primary key' in the 'export' section of your schema. The following example duplicates the default functionality for the myobj.
 
 <pre>
 /**
-* Implementation of hook_menu().
+* Save a single myobj.
 */
-function mymodule_menu() {
-  $items['admin/build/mymodule/import'] = array(
-    'title' => 'Import',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('mymodule_export_myobj'),
-    'access arguments' => array('mymodule import permission'),
-    'type' => MENU_CALLBACK,
-  );
-
-  return $items;
-}
-</pre>
-
-In this case, the page callback is a form, where the user can paste the exported code. We can provide an optional name field to change the name of the object while importing. This can be used as a poor man's clone. The method used here can also be used to very easily clone objects without going through the export/import process manually.
-
-<pre>
-/**
- * Form from page callback to import a myobj
- */
-function mymodule_export_myobj(&$form, &$form_state) {
-  $form['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Myobj name'),
-    '#description' => t('Enter the name of the new myobj. This is optional and is not necessary if you do not wish to rename the object.'),
-  );
-
-  $form['object'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Paste variant code here'),
-    '#rows' => 15,
-  );
-}
-
-/**
- * Make sure that an import actually provides a handler.
- */
-function mymodule_export_myobj_validate($form, &$form_state) {
-  // First, run the PHP and turn the input code into an object.
-  ob_start();
-  eval($form_state['values']['object']);
-  ob_end_clean();
-
-  // The object should appear as $myobj. This was the "identifier" set in the export section of the schema.
-  if (empty($myobj)) {
-    $errors = ob_get_contents();
-    if (empty($errors)) {
-      $errors = t('No myobj found.');
-    }
-
-    form_error($form['object'], t('Unable to get a myobj from the import. Errors reported: @errors', array('@errors' => $errors)));
-  }
-
-  $form_state['obj'] = obj;
-}
-
-/**
- * Save the imported object.
- */
-function page_manager_handler_import_submit($form, &$form_state) {
-  $myobj = $form_state['obj'];
-
-  if (!empty($form_state['values']['name'])) {
-    $myobj->name = $form_state['values']['name'];
-  }
-
-  mymodule_myobj_save($myobj);
-  $form_state['redirect'] = 'admin/build/mymodule/' . $myobj->name . '/edit';
+function mymodule_myobj_save(&$myobj) {
+  $update = (isset($myobj->oid) && is_numeric($myobj->oid)) ? array('oid') : array();
+  return drupal_write_record('myobj', $myobj, $update);
 }
 </pre>
 
-<h3>Enabling and Disabling exported objects</h3>
-
-This section still needs to be written.
-
-<h3>Putting exported objects into code</h3>
-
-This section still needs to be written.
+<h3>Default hooks for your exports</h3>
+All exportables come with a 'default' hook, which can be used to put your exportable into code. The easiest way to actually use this hook is to set up your exportable for bulk exporting, enable the bulk export module and export an object.
index 77f2498..1c7be9e 100644 (file)
@@ -56,22 +56,41 @@ function panels_ctools_plugin_layouts() {
 The following information can be specified:
 <dl>
 <dt>cache</dt>
-<dd>If set to TRUE, the results of ctools_get_plugins will be cached in the 'cache' table, thus preventing .inc files from being loaded. ctools_get_plugins looking for a specific plugin will always load the appropriate .inc file.</dd>
+<dd><em>Defaults to:</em> <strong>FALSE</strong></dd>
+<dd>If set to TRUE, the results of ctools_get_plugins will be cached in the 'cache' table (by default), thus preventing .inc files from being loaded. ctools_get_plugins looking for a specific plugin will always load the appropriate .inc file.</dd>
+<dt>cache table</dt>
+<dd><em>Defaults to:</em> <strong>'cache'</strong></dd>
+<dd>If 'cache' is TRUE, then this value specifies the cache table where the cached plugin information will be stored.</dd>
 <dt>defaults</dt>
-<dd>An array of defaults that should be applied to each plugin; this can be used to ensure that every plugin has the basic data necessary. These defaults will not ovewrite data supplied by the plugin. This could also be a function name, in which case the callback will be used to provide defaults.</dd>
+<dd><em>Defaults to:</em> <strong>array()</strong></dd>
+<dd>An array of defaults that should be added to each plugin; this can be used to ensure that every plugin has the basic data necessary. These defaults will not ovewrite data supplied by the plugin. This could also be a function name, in which case the callback will be used to provide defaults. NOTE, however, that the callback-based approach is deprecated as it is redundant with the 'process' callback, and as such will be removed in later versions. Consequently, you should only use the array form for maximum cross-version compatibility.</dd>
 <dt>load themes</dt>
-<dd>If set to TRUE, then plugins can be supplied by themes as well as modules. If this is the case, all themes that are currently enabled will provide a plugin: NOTE: Due to a slight UI bug in Drupal, it is possible for the default theme to be active but not enabled. If this is the case, that theme will NOT provide plugins, so if you are using this feature, be sure to document that issue. Also, themes set via $custom_theme do not necessarily need to be enabled, but the system has no way of knowing what those themes are, so the enabled flag is the only true method of identifying which themes can provide layouts.</dd>
+<dd><em>Defaults to:</em> <strong>FALSE</strong></dd>
+<dd>If set to TRUE, then plugins of this type can be supplied by themes as well as modules. If this is the case, all themes that are currently enabled will provide a plugin: NOTE: Due to a slight UI bug in Drupal, it is possible for the default theme to be active but not enabled. If this is the case, that theme will NOT provide plugins, so if you are using this feature, be sure to document that issue. Also, themes set via $custom_theme do not necessarily need to be enabled, but the system has no way of knowing what those themes are, so the enabled flag is the only true method of identifying which themes can provide layouts.</dd>
 <dt>hook</dt>
-<dd>The name of the hook used to collect data for this plugin. Normally this is $module . '_' . $type -- but this can be changed here. If you change this, you MUST be sure to document this for your plugin implementors as it will change the format of the specially named hook.
+<dd><em>Defaults to:</em> (dynamic value)</dd>
+<dd>The name of the hook used to collect data for this plugin. Normally this is <strong>$module . '_' . $type</strong> -- but this can be changed here. If you change this, you MUST be sure to document this for your plugin implementors as it will change the format of the specially named hook.
 <dt>process</dt>
-<dd>An optional function callback to use for processing a plugin. This can be used to provide automated settings that require code. The parameters on this callback are: <strong>callback(&$plugin, $info)</strong> where $plugin is a reference to the plugin as processed and $info is the fully processed result of hook_ctools_plugin_api_info().
+<dd><em>Defaults to:</em> <strong>''</strong></dd>
+<dd>An optional function callback to use for processing a plugin. This can be used to provide automated settings that must be calculated per-plugin instance (i.e., it is not enough to simply append an array via 'defaults'). The parameters on this callback are: <strong>callback(&$plugin, $info)</strong> where $plugin is a reference to the plugin as processed and $info is the fully processed result of hook_ctools_plugin_api_info().
+<dt>extension</dt>
+<dd><em>Defaults to:</em> <strong>'inc'</strong></dd>
+<dd>Can be used to change the extension on files containing plugins of this type. By default the extension will be "inc", though it will default to "info" if "info files" is set to true. Do not include the dot in the extension if changing it, that will be added automatically.</dd>
 <dt>info file</dt>
+<dd><em>Defaults to:</em> <strong>FALSE</strong></dd>
 <dd>If set to TRUE, then the plugin will look for a .info file instead of a .inc. Internally, this will look exactly the same, though obviously a .info file cannot contain functions. This can be good for styles that may not need to contain code.</dd>
-<dt>extension</dt>
-<dd>Can be used to change the extension on a file. By default the extension will be "inc", though it will default to "info" if "info files" is set to true. Do not include the dot in the extension if changing it, that will be added automatically.</dd>
+<dt>use hooks</dt>
+<dd><em>Defaults to:</em> <strong>TRUE</strong>*</dd>
+<dd>Use to enable support for plugin definition hooks instead of plugin definition files. NOTE: using a central plugin definition hook is less optimal for the plugins system, and as such this will default to FALSE in later versions.</dd>
+<dt>child plugins</dt>
+<dd><em>Defaults to:</em> <strong>FALSE</strong></dd>
+<dd>If set to TRUE, the plugin type can automatically have 'child plugins' meaning each plugin can actually provide multiple plugins. This is mostly used for plugins that store some of their information in the database, such as views, blocks or exportable custom versions of plugins.</dd>
+<dd>To implement, each plugin can have a 'get child' and 'get children' callback. Both of these should be implemented for performance reasons, since it is best to avoid getting all children if necessary, but if 'get child' is not implemented, it will fall back to 'get children' if it has to.</dd>
+<dd>Child plugins should be named parent:child, with the : being the separator, so that it knows which parent plugin to ask for teh child. The 'get children' method should at least return the parent plugin as part of the list, unless it wants the parent plugin itself to not be a choosable option, which is not unheard of. </dd>
+<dd>'get children' arguments are ($plugin, $parent) and 'get child' arguments are ($plugin, $parent, $child).
 </dl>
 
-In addition, there is a 'module', 'type' and 'hook' settings; these are for internal use of the plugin system and you should not change these.
+In addition, there is a 'module' and 'type' settings; these are for internal use of the plugin system and you should not change these.
 
 <h3>Using the data</h3>
 
@@ -145,7 +164,7 @@ Or like this:
   ),
 </pre>
 
-An example, for 'plugin_example' type 
+An example, for 'plugin_example' type
 
 <pre>
 $plugin = array(
index a85ec34..c306b8a 100644 (file)
@@ -1,5 +1,5 @@
 <!-- $Id$ -->
-The plugins tool allows a module to allow <b>other</b> modules (and themes!) to provide plugins which provide some kind of functionality or some kind of task. For example, in Panels there are several types of plugins: Content types (which are like blocks), layouts (which are page layouts) and styles (which can be used to style a panel). Each plugin is represented by a .inc file, and the functionaly they offer can differ wildly.
+The plugins tool allows a module to allow <b>other</b> modules (and themes!) to provide plugins which provide some kind of functionality or some kind of task. For example, in Panels there are several types of plugins: Content types (which are like blocks), layouts (which are page layouts) and styles (which can be used to style a panel). Each plugin is represented by a .inc file, and the functionality they offer can differ wildly.
 
 A module which uses plugins can implement a hook describing the plugin (which is not necessary, as defaults will be filled in) and then calls a ctools function which loads either all the known plugins (used for listing/choosing) or loads a specific plugin (used when its known which plugin is needed). From the perspective of the plugin system, a plugin is a packet of data, usually some printable info and a list of callbacks. It is up to the module implementing plugins to determine what that info means and what the callbacks do.
 
index d5ec5f7..0d66344 100644 (file)
@@ -253,3 +253,36 @@ function wizardid_step2_form_submit($form, &$form_state) {
 </pre>
 
 The data is stored in the <em>my data</em> object on submitting. If the user goes back to this step the cached <em>my data</em> is used as the default form value. The function <em>my_module_get_cache()</em> is like the cache functions explained above.
+
+<h3>Required fields, cancel and back buttons</h3>
+If you have required fields in your forms, the back and cancel buttons will not work as expected since validation of the form will fail. You can add the following code to the top of your form validation to avoid this problem :
+<pre>
+/**
+ * Validation handler for step2 form
+ */
+function wizardid_step2_form_validate(&$form, &$form_state) {
+  // if the clicked button is anything but the normal flow
+  if ($form_state['clicked_button']['#next'] != $form_state['next']) {
+    drupal_get_messages('error');
+    form_set_error(NULL, '', TRUE);
+    return;
+  }
+  // you form validation goes here
+  // ...
+}
+</pre>
+
+<h3>Wizard for anonymous users</h3>
+If you are creating a wizard which is be used by anonymous users, you might run into some issues with drupal's caching for anonymous users. You can circumvent this by using hook_init and telling drupal to not cache your wizard pages :
+<pre>
+/**
+ * Implementation of hook init
+ */
+function mymodule_init() {
+  // if the path leads to the wizard
+  if (drupal_match_path($_GET['q'], 'path/to/your/wizard/*')) {
+    // set cache to false
+    $GLOBALS['conf']['cache'] = FALSE;   
+  }
+}
+</pre>
index ec3532d..5c65998 100644 (file)
@@ -1,6 +1,9 @@
 <?php
 // $Id$
 
+// Set this so we can tell that the file has been included at some point.
+define('CTOOLS_AJAX_INCLUDED', 1);
+
 /**
  * @file
  * Utilize the CTools AJAX responder.
@@ -281,9 +284,9 @@ function ctools_ajax_command_changed($selector, $star = '') {
 }
 
 /**
- * Create a changed command for the AJAX responder.
+ * Create a css command for the AJAX responder.
  *
- * This will mark an item as 'changed'.
+ * This will directly add CSS to the page.
  *
  * @param $selector
  *   The CSS selector. This can be any selector jquery uses in $().
@@ -301,6 +304,22 @@ function ctools_ajax_command_css($selector, $argument) {
 /**
  * Create a settings command for the AJAX responder.
  *
+ * This will add CSS files to the output. Files that have already
+ * been processed will not be processed again.
+ *
+ * @param $argument
+ *   An array of CSS files.
+ */
+function ctools_ajax_command_css_files($argument) {
+  return array(
+    'command' => 'css_files',
+    'argument' => $argument,
+  );
+}
+
+/**
+ * Create a settings command for the AJAX responder.
+ *
  * This will extend Drupal.settings with the given array.
  *
  * @param $argument
@@ -314,6 +333,22 @@ function ctools_ajax_command_settings($argument) {
 }
 
 /**
+ * Create a settings command for the AJAX responder.
+ *
+ * This will add javascript files to the output. Files that have already
+ * been processed will not be processed again.
+ *
+ * @param $argument
+ *   An array of javascript files.
+ */
+function ctools_ajax_command_scripts($argument) {
+  return array(
+    'command' => 'scripts',
+    'argument' => $argument,
+  );
+}
+
+/**
  * Create a data command for the AJAX responder.
  *
  * This will attach the name=value pair of data to the selector via
@@ -376,11 +411,14 @@ function ctools_ajax_command_restripe($selector) {
  * @param $url
  *   The url to be redirected to. This can be an absolute URL or a
  *   Drupal path.
+ * @param $delay
+ *   A delay before applying the redirection, in milliseconds.
  */
-function ctools_ajax_command_redirect($url) {
+function ctools_ajax_command_redirect($url, $delay = 0) {
   return array(
     'command' => 'redirect',
     'url' => url($url),
+    'delay' => $delay,
   );
 }
 
@@ -415,6 +453,58 @@ function ctools_ajax_command_submit($selector) {
  * to the AJAX requester.
  */
 function ctools_ajax_render($commands = array()) {
+  $js_files = array();
+  $settings = ctools_process_js_files($js_files, 'header');
+  $settings += ctools_process_js_files($js_files, 'footer');
+
+  $query_string = '?'. substr(variable_get('css_js_query_string', '0'), 0, 1);
+  $css = drupal_add_css();
+  foreach ($css as $media => $types) {
+    // If CSS preprocessing is off, we still need to output the styles.
+    // Additionally, go through any remaining styles if CSS preprocessing is on and output the non-cached ones.
+    foreach ($types as $type => $files) {
+      if ($type == 'module') {
+        // Setup theme overrides for module styles.
+        $theme_styles = array();
+        foreach (array_keys($css[$media]['theme']) as $theme_style) {
+          $theme_styles[] = basename($theme_style);
+        }
+      }
+      // The theme stuff should already be added and because of admin themes,
+      // this could cause different CSS to be added.
+      if ($type != 'theme') {
+        foreach ($types[$type] as $file => $preprocess) {
+          // If the theme supplies its own style using the name of the module style, skip its inclusion.
+          // This includes any RTL styles associated with its main LTR counterpart.
+          if ($type == 'module' && in_array(str_replace('-rtl.css', '.css', basename($file)), $theme_styles)) {
+            // Unset the file to prevent its inclusion when CSS aggregation is enabled.
+            unset($types[$type][$file]);
+            continue;
+          }
+          // Only include the stylesheet if it exists.
+          if (file_exists($file)) {
+            $css_files[] = array(
+              'file' => base_path() . $file . $query_string,
+              'media' => $media,
+            );
+          }
+        }
+      }
+    }
+  }
+
+  if (!empty($js_files)) {
+    array_unshift($commands, ctools_ajax_command_scripts(array_keys($js_files)));
+  }
+
+  if (!empty($css_files)) {
+    array_unshift($commands, ctools_ajax_command_css_files($css_files));
+  }
+
+  if (!empty($settings)) {
+    array_unshift($commands, ctools_ajax_command_settings(call_user_func_array('array_merge_recursive', $settings)));
+  }
+
   if (!empty($_REQUEST['ctools_multipart'])) {
     // We don't use drupal_json here because the header is not true. We're not really
     // returning JSON, strictly-speaking, but rather JSON content wrapped in a <textarea>
@@ -454,26 +544,32 @@ function ctools_ajax_render_error($error = '') {
  *   ctools_ajax_associate_url_to_element($form, $form['example'], 'example/ajax/urlpath');
  * @endcode
  *
- * The AJAX request will POST the value of the form element in the "ctools_changed" parameter.
+ * The AJAX request will POST the value of the form element in the
+ * "ctools_changed" parameter (i.e. $_POST['ctools_changed']).
  *
  * @param &$form
- *   Reference to the form element. This is required to have the #id and #attribute
- *   elements populated and to create the hidden form element for each select.
+ *   Reference to the form element. This is required to have the #id and
+ *   #attribute elements populated and to create the hidden form element for
+ *   each select.
  * @param &$form_element
  *   The form element we are going to take action on.
  * @param $dest
  *   The URL to associate the form element to.
  * @param $type
- *   A type to use, in case a different behavior should be attached. Defaults
- *   to ctools-use-ajax.
+ *   Optional; A type to use, in case a different behavior should be attached.
+ *   If empty the type will be set to "ctools-use-ajax" for submit elements and
+ *   "ctools-use-ajax-onchange" for other elements.
  */
-function ctools_ajax_associate_url_to_element(&$form, &$form_element, $dest, $type = 'ctools-use-ajax') {
+function ctools_ajax_associate_url_to_element(&$form, &$form_element, $dest, $type = '') {
   drupal_add_js('misc/jquery.form.js', 'core');
   if (!isset($form_element['#id'])) {
     //Create a unique ID to associate $form_element and hidden elements since we dont have an ID
     $form_element['#id'] = uniqid('ctools-ajax-url-');
 
-    // FIXME bad merge?
+    if (empty($type)) {
+      $type = $form_element['#type'] == 'submit' ? 'ctools-use-ajax' : 'ctools-use-ajax-onchange';
+    }
+
     if (empty($form_element['#attributes']['class'])) {
       $form_element['#attributes']['class'] = array($type);
     }
@@ -489,3 +585,95 @@ function ctools_ajax_associate_url_to_element(&$form, &$form_element, $dest, $ty
     '#attributes' => array('class' => array($form_element['#id'] . '-url')),
   );
 }
+
+function ctools_ajax_page_preprocess(&$variables) {
+  $js_files = $css_files = array();
+  ctools_process_js_files($js_files, 'header');
+  ctools_process_js_files($js_files, 'footer');
+  ctools_process_css_files($css_files, $variables['css']);
+
+  // Add loaded JS and CSS information to the footer, so that an AJAX
+  // request knows if they are already loaded.
+  // For inline Javascript to validate as XHTML, all Javascript containing
+  // XHTML needs to be wrapped in CDATA. To make that backwards compatible
+  // with HTML 4, we need to comment out the CDATA-tag.
+  $loaded = array('CToolsAJAX' => array('scripts' => $js_files, 'css' => $css_files));
+  $embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
+  $embed_suffix = "\n//--><!]]>\n";
+  $variables['closure'].= '<script type="text/javascript">' . $embed_prefix . 'jQuery.extend(Drupal.settings, ' . drupal_to_js($loaded) . ");" . $embed_suffix . "</script>\n";
+}
+
+/**
+ * Create a list of javascript files that are on the page.
+ */
+function ctools_process_js_files(&$js_files, $scope) {
+  // Automatically extract any 'settings' added via drupal_add_js() and make
+  // them the first command.
+  $scripts = drupal_add_js(NULL, NULL, $scope);
+
+  // Get replacements that are going to be made by contrib modules and take
+  // them into account so we don't double-load scripts.
+  static $replacements = NULL;
+  if (!isset($replacements)) {
+    $replacements = module_invoke_all('js_replacements');
+  }
+
+  $settings = array();
+  foreach ($scripts as $type => $data) {
+    switch ($type) {
+      case 'setting':
+        $settings = $data;
+        break;
+      case 'inline':
+      case 'theme':
+        // Presently we ignore inline javascript.
+        // Theme JS is already added and because of admin themes, this could add
+        // improper JS to the page.
+        break;
+      default:
+        // If JS preprocessing is off, we still need to output the scripts.
+        // Additionally, go through any remaining scripts if JS preprocessing is on and output the non-cached ones.
+        foreach ($data as $path => $info) {
+          // If the script is being replaced, take that replacment into account.
+          $final_path = isset($replacements[$type][$path]) ? $replacements[$type][$path] : $path;
+          $js_files[base_path() . $final_path] = TRUE;
+        }
+    }
+  }
+
+  return $settings;
+}
+
+/**
+ * Create a list of CSS files to add to the page.
+ */
+function ctools_process_css_files(&$css_files, $css) {
+  // Go through all CSS files that are being added to the page and catalog them.
+  $css_files = array();
+  foreach ($css as $media => $types) {
+    // If CSS preprocessing is off, we still need to output the styles.
+    // Additionally, go through any remaining styles if CSS preprocessing is on and output the non-cached ones.
+    foreach ($types as $type => $files) {
+      if ($type == 'module') {
+        // Setup theme overrides for module styles.
+        $theme_styles = array();
+        foreach (array_keys($css[$media]['theme']) as $theme_style) {
+          $theme_styles[] = basename($theme_style);
+        }
+      }
+      foreach ($types[$type] as $file => $preprocess) {
+        // If the theme supplies its own style using the name of the module style, skip its inclusion.
+        // This includes any RTL styles associated with its main LTR counterpart.
+        if ($type == 'module' && in_array(str_replace('-rtl.css', '.css', basename($file)), $theme_styles)) {
+          // Unset the file to prevent its inclusion when CSS aggregation is enabled.
+          unset($types[$type][$file]);
+          continue;
+        }
+        // Only include the stylesheet if it exists.
+        if (file_exists($file)) {
+          $css_files[base_path() . $file] = TRUE;
+        }
+      }
+    }
+  }
+}
index 9052ee2..d17866a 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-// $Id$
+// $Id $
 
 /**
  * @file
@@ -169,7 +169,7 @@ function ctools_cleanstring($string, $settings = array()) {
 
   // Enforce the maximum component length
   if (!empty($settings['max length'])) {
-    $output = _pathauto_truncate_chars($output, $settings['max length'], $settings['separator']);
+    $output = ctools_cleanstring_truncate($output, $settings['max length'], $settings['separator']);
   }
 
   if (!empty($settings['lower case'])) {
index d0acd98..9859043 100644 (file)
@@ -18,7 +18,7 @@
 function ctools_ctools_plugin_content_types() {
   return array(
     'cache' => FALSE,
-    'defaults' => 'ctools_content_defaults',
+    'process' => 'ctools_content_process',
   );
 }
 
@@ -28,7 +28,7 @@ function ctools_ctools_plugin_content_types() {
  * Currently we check for automatically named callbacks to make life a little
  * easier on the developer.
  */
-function ctools_content_defaults($info, &$plugin) {
+function ctools_content_process(&$plugin, $info) {
   $function_base = $plugin['module'] . '_' . $plugin['name'] . '_content_type_';
 
   if (empty($plugin['render callback']) && function_exists($function_base . 'render')) {
@@ -62,9 +62,11 @@ function ctools_content_defaults($info, &$plugin) {
 
   // Another ease of use check:
   if (!isset($plugin['content types'])) {
-    // If a content type is set to SINGLE and *no* subtypes are defined, this rewrites
-    // things so that the syntax is nicer.
-    if (!empty($plugin['single'])) {
+    // If a subtype plugin exists, try to use it. Otherwise assume single.
+    if (function_exists($function_base . 'content_types')) {
+      $plugin['content types'] = $function_base . 'content_types';
+    }
+    else {
       $type = array(
         'title' => $plugin['title'],
         'description' => $plugin['description'],
@@ -79,10 +81,9 @@ function ctools_content_defaults($info, &$plugin) {
         $type['top level'] = $plugin['top level'];
       }
       $plugin['content types'] = array($plugin['name'] => $type);
-    }
-    // Otherwise, auto discover the function based upon pattern naming.
-    else if (function_exists($function_base . 'content_types')) {
-      $plugin['content types'] = $function_base . 'content_types';
+      if (!isset($plugin['single'])) {
+        $plugin['single'] = TRUE;
+      }
     }
   }
 }
@@ -137,6 +138,10 @@ function ctools_content_get_subtypes($type) {
     $plugin = ctools_get_content_type($type);
   }
 
+  if (empty($plugin) || empty($plugin['name'])) {
+    return;
+  }
+
   if (isset($cache[$plugin['name']])) {
     return $cache[$plugin['name']];
   }
@@ -262,7 +267,14 @@ function ctools_content_render($type, $subtype, $conf, $keywords = array(), $arg
     $plugin = ctools_get_content_type($type);
   }
 
-  if ($function = ctools_plugin_get_function($plugin, 'render callback')) {
+  $subtype_info = ctools_content_get_subtype($plugin, $subtype);
+
+  $function = ctools_plugin_get_function($subtype_info, 'render callback');
+  if (!$function) {
+    $function = ctools_plugin_get_function($plugin, 'render callback');
+  }
+
+  if ($function) {
     $pane_context = ctools_content_select_context($plugin, $subtype, $conf, $context);
     if ($pane_context === FALSE) {
       return;
@@ -316,6 +328,24 @@ function ctools_content_render($type, $subtype, $conf, $keywords = array(), $arg
 }
 
 /**
+ * Determine if a content type can be edited or not.
+ *
+ * Some content types simply have their content and no options. This function
+ * lets a UI determine if it should display an edit link or not.
+ */
+function ctools_content_editable($type, $subtype, $conf) {
+  if (empty($type['edit form']) && empty($subtype['edit form'])) {
+    return FALSE;
+  }
+
+  if ($function = ctools_plugin_get_function($subtype, 'check editable')) {
+    return $function($type, $subtype, $conf);
+  }
+
+  return TRUE;
+}
+
+/**
  * Get the administrative title from a given content type.
  *
  * @param $type
@@ -341,7 +371,10 @@ function ctools_content_admin_title($type, $subtype, $conf, $context = NULL) {
   if ($function = ctools_plugin_get_function($plugin, 'admin title')) {
     $pane_context = ctools_content_select_context($plugin, $subtype, $conf, $context);
     if ($pane_context === FALSE) {
-      return t('@type:@subtype will not display due to missing context', array('@type' => $plugin['name'], '@subtype' => '@subtype'));
+      if ($plugin['name'] == $subtype) {
+        return t('@type will not display due to missing context', array('@type' => $plugin['name']));
+      }
+      return t('@type:@subtype will not display due to missing context', array('@type' => $plugin['name'], '@subtype' => $subtype));
     }
 
     return $function($subtype, $conf, $pane_context);
@@ -508,7 +541,7 @@ function ctools_content_configure_form_defaults_submit(&$form, &$form_state) {
  *
  * The $form_info and $form_state need to be preconfigured with data you'll need
  * such as whether or not you're using ajax, or the modal. $form_info will need
- * your next/submit callbacks so that you can cache your data appropriate.
+ * your next/submit callbacks so that you can cache your data appropriately.
  *
  * @return
  *   If this function returns false, no form exists.
@@ -528,12 +561,23 @@ function ctools_content_form($op, $form_info, &$form_state, $plugin, $subtype_na
   );
 
   // Turn the forms defined in the plugin into the format the wizard needs.
-  if ($op == 'add' && isset($plugin['add form'])) {
-    _ctools_content_create_form_info($form_info, $plugin['add form'], $plugin, $subtype, $op);
+  if ($op == 'add') {
+    if (!empty($subtype['add form'])) {
+      _ctools_content_create_form_info($form_info, $subtype['add form'], $subtype, $subtype, $op);
+    }
+    else if (!empty($plugin['add form'])) {
+      _ctools_content_create_form_info($form_info, $plugin['add form'], $plugin, $subtype, $op);
+    }
   }
-  // Use the edit form for the add form if add form was completely left off.
-  else if (isset($plugin['edit form'])) {
-    _ctools_content_create_form_info($form_info, $plugin['edit form'], $plugin, $subtype, $op);
+
+  if (empty($form_info['order'])) {
+    // Use the edit form for the add form if add form was completely left off.
+    if (!empty($subtype['edit form'])) {
+      _ctools_content_create_form_info($form_info, $subtype['edit form'], $subtype, $subtype, $op);
+    }
+    else if (!empty($plugin['edit form'])) {
+      _ctools_content_create_form_info($form_info, $plugin['edit form'], $plugin, $subtype, $op);
+    }
   }
 
   if (empty($form_info['order'])) {
index 51ee408..ee71456 100644 (file)
@@ -38,6 +38,10 @@ function ctools_content_autocomplete_node($string) {
       $arg = array(':title' => '%' . $string . '%');
       $where = "LOWER(n.title) LIKE LOWER(:title)";
     }
+    if (!user_access('administer nodes')) {
+      $where .= ' AND n.status = 1';
+    }
+
     $result = db_query_range('SELECT n.nid, n.title, u.name FROM {node} n INNER JOIN {users} u ON u.uid = n.uid WHERE ' . $where, 0, 10, $arg);
 
     $matches = array();
index a30edc1..56c773b 100644 (file)
  * the original settings.
  *
  * The callbacks:
- * - $module . _ctools_access_cache_get($argument) -- get the 'access' settings
+ * - $module . _ctools_access_get($argument) -- get the 'access' settings
  *   from cache. Must return array($access, $contexts); This callback can
  *   perform access checking to make sure this URL is not being gamed.
- * - $module . _ctools_access_cache_set($argument, $access) -- set the 'access'
+ * - $module . _ctools_access_set($argument, $access) -- set the 'access'
  *   settings in cache.
- * - $module . _ctools_access_cache_clear($argument) -- clear the cache.
+ * - $module . _ctools_access_clear($argument) -- clear the cache.
  *
  * The ctools_object_cache is recommended for this purpose, but you can use
  * any caching mechanism you like. An example:
@@ -385,7 +385,7 @@ function ctools_access_ajax_edit($fragment = NULL, $id = NULL) {
 }
 
 /**
- * From to edit the settings of an access test.
+ * Form to edit the settings of an access test.
  */
 function ctools_access_ajax_edit_item($form, &$form_state) {
   $test = &$form_state['test'];
@@ -394,12 +394,17 @@ function ctools_access_ajax_edit_item($form, &$form_state) {
   if (isset($plugin['required context'])) {
     $form['context'] = ctools_context_selector($form_state['contexts'], $plugin['required context'], $test['context']);
   }
-
   $form['settings'] = array('#tree' => TRUE);
   if ($function = ctools_plugin_get_function($plugin, 'settings form')) {
     $function($form, $form_state, $test['settings']);
   }
 
+  $form['not'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Reverse (NOT)'),
+    '#default_value' => !empty($test['not']),
+  );
+
   $form['save'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
@@ -429,6 +434,7 @@ function ctools_access_ajax_edit_item_submit($form, &$form_state) {
   if (isset($form_state['values']['context'])) {
     $form_state['test']['context'] = $form_state['values']['context'];
   }
+  $form_state['test']['not'] = !empty($form_state['values']['not']);
 }
 
 /**
index d66632f..d1ad20b 100644 (file)
@@ -138,12 +138,16 @@ function ctools_context_data($type, $name) {
 /**
  * Add the argument table plus gadget plus javascript to the form.
  */
-function ctools_context_add_argument_form($module, &$form, &$form_state, &$form_location, $object) {
+function ctools_context_add_argument_form($module, &$form, &$form_state, &$form_location, $object, $name = NULL) {
+  if (empty($name)) {
+    $name = $object->name;
+  }
+
   $form_location = array(
     '#prefix' => '<div id="ctools-arguments-table">',
     '#suffix' => '</div>',
     '#theme' => 'ctools_context_item_form',
-    '#object_name' => $object->name,
+    '#object_name' => $name,
     '#ctools_context_type' => 'argument',
     '#ctools_context_module' => $module,
   );
@@ -161,12 +165,16 @@ function ctools_context_add_argument_form($module, &$form, &$form_state, &$form_
   }
 }
 
-function ctools_context_add_context_form($module, &$form, &$form_state, &$form_location, $object) {
+function ctools_context_add_context_form($module, &$form, &$form_state, &$form_location, $object, $name = NULL) {
+  if (empty($name)) {
+    $name = $object->name;
+  }
+
   $form_location = array(
     '#prefix' => '<div id="ctools-contexts-table">',
     '#suffix' => '</div>',
     '#theme' => 'ctools_context_item_form',
-    '#object_name' => $object->name,
+    '#object_name' => $name,
     '#ctools_context_type' => 'context',
     '#ctools_context_module' => $module,
   );
@@ -190,12 +198,16 @@ function ctools_context_add_context_form($module, &$form, &$form_state, &$form_l
   }
 }
 
-function ctools_context_add_required_context_form($module, &$form, &$form_state, &$form_location, $object) {
+function ctools_context_add_required_context_form($module, &$form, &$form_state, &$form_location, $object, $name = NULL) {
+  if (empty($name)) {
+    $name = $object->name;
+  }
+
   $form_location = array(
     '#prefix' => '<div id="ctools-requiredcontexts-table">',
     '#suffix' => '</div>',
     '#theme' => 'ctools_context_item_form',
-    '#object_name' => $object->name,
+    '#object_name' => $name,
     '#ctools_context_type' => 'requiredcontext',
     '#ctools_context_module' => $module,
   );
@@ -217,12 +229,16 @@ function ctools_context_add_required_context_form($module, &$form, &$form_state,
   }
 }
 
-function ctools_context_add_relationship_form($module, &$form, &$form_state, &$form_location, $object) {
+function ctools_context_add_relationship_form($module, &$form, &$form_state, &$form_location, $object, $name = NULL) {
+  if (empty($name)) {
+    $name = $object->name;
+  }
+
   $form_location = array(
     '#prefix' => '<div id="ctools-relationships-table">',
     '#suffix' => '</div>',
     '#theme' => 'ctools_context_item_form',
-    '#object_name' => $object->name,
+    '#object_name' => $name,
     '#ctools_context_type' => 'relationship',
     '#ctools_context_module' => $module,
   );
@@ -792,7 +808,7 @@ function ctools_edit_relationship_form_submit($form, &$form_state) {
   $relationship = $form_state['info'];
 
   if (isset($relationship['settings form submit']) && function_exists($relationship['settings form submit'])) {
-    $relationship['settings form submit']($form, $form_state['values']['relationship_settings'], $form_state);
+    $relationship['settings form submit']($form, $form_state['values']['relationship']['relationship_settings'], $form_state);
   }
 
   $form_state['ref'][$form_state['position']] = $form_state['values']['relationship'];
index f464c9a..4d319f0 100644 (file)
@@ -44,7 +44,7 @@ function ctools_context_handler_render($task, $subtask, $contexts, $args, $page
   foreach ($handlers as $handler) {
     if ($function = page_manager_get_renderer($handler)) {
       if ($info = $function($handler, $contexts, $args)) {
-        drupal_alter('ctools_render', $info, $page, $args, $contexts, $task, $subtask);
+        drupal_alter('ctools_render', $info, $page, $args, $contexts, $task, $subtask, $handler);
         // If we don't own the page, let the caller deal with rendering.
         if (!$page) {
           return $info;
@@ -66,6 +66,27 @@ function ctools_context_handler_render($task, $subtask, $contexts, $args, $page
           'handler' => $handler,
         ));
 
+        if (!empty($info['response code']) && $info['response code'] != 200) {
+          switch ($info['response code']) {
+            case 403:
+              return MENU_ACCESS_DENIED;
+            case 404:
+              return MENU_NOT_FOUND;
+            case 301:
+            case 302:
+            case 303:
+            case 304:
+            case 305:
+            case 307:
+              $info += array(
+                'query' => '',
+                'fragment' => '',
+              );
+              return drupal_goto($info['destination'], $info['query'], $info['fragment'], $info['response code']);
+            // @todo -- should other response codes be supported here?
+          }
+        }
+
         /*
         // Only do this if something hasn't already changed the active menu,
         // such as a book.
@@ -114,9 +135,22 @@ function ctools_context_handler_pre_render($handler, $contexts, $args) {
   if (user_access('administer page manager') && isset($handler->task)) {
     // Provide a tab to edit this context:
     ctools_include('menu');
+    $task = page_manager_get_task($handler->task);
+
+    $title = !empty($task['tab title']) ? $task['tab title'] : t('Edit @type', array('@type' => $plugin['title']));
+    $trail = array();
+    if (!empty($plugin['tab operation'])) {
+      if (is_array($plugin['tab operation'])) {
+        $trail = $plugin['tab operation'];
+      }
+      else if (function_exists($plugin['tab operation'])) {
+        $trail = $plugin['tab operation']($handler, $contexts, $args);
+      }
+    }
+
     ctools_menu_add_tab(array(
-      'title' => t('Edit @type', array('@type' => $plugin['title'])),
-      'href' => page_manager_edit_url(page_manager_make_task_name($handler->task, $handler->subtask)),
+      'title' => $title,
+      'href' => page_manager_edit_url(page_manager_make_task_name($handler->task, $handler->subtask), $trail),
     ));
   }
 }
@@ -188,8 +222,7 @@ function ctools_context_handler_summary($task, $subtask, $handler) {
  * for use in rendering.
  */
 function ctools_context_handler_get_task_contexts($task, $subtask, $args) {
-  $contexts = array();
-
+  $contexts = ctools_context_handler_get_base_contexts($task, $subtask);
   $arguments = ctools_context_handler_get_task_arguments($task, $subtask);
   ctools_context_get_context_from_arguments($arguments, $contexts, $args);
 
@@ -216,8 +249,10 @@ function ctools_context_handler_get_handler_contexts($contexts, $handler) {
  * the contexts as placeholders.
  */
 function ctools_context_handler_get_all_contexts($task, $subtask, $handler) {
+  $contexts = array();
+
   $object = ctools_context_handler_get_task_object($task, $subtask, $handler);
-  $contexts = ctools_context_load_contexts($object, TRUE);
+  $contexts = ctools_context_load_contexts($object, TRUE, $contexts);
   ctools_context_handler_set_access_restrictions($task, $subtask, $handler, $contexts);
   return $contexts;
 }
@@ -243,6 +278,7 @@ function ctools_context_handler_get_handler_object($handler) {
 function ctools_context_handler_get_task_object($task, $subtask, $handler) {
   $object = new stdClass;
   $object->name = !empty($handler->name) ? $handler->name : 'temp';
+  $object->base_contexts = ctools_context_handler_get_base_contexts($task, $subtask, TRUE);
   $object->arguments = ctools_context_handler_get_task_arguments($task, $subtask);
   $object->contexts = isset($handler->conf['contexts']) ? $handler->conf['contexts'] : array();
   $object->relationships = isset($handler->conf['relationships']) ? $handler->conf['relationships'] : array();
@@ -251,6 +287,20 @@ function ctools_context_handler_get_task_object($task, $subtask, $handler) {
 }
 
 /**
+ * Get base contexts from a task, if it has any.
+ *
+ * Tasks can get their contexts either from base contexts or arguments; base
+ * contexts extract their information from the environment.
+ */
+function ctools_context_handler_get_base_contexts($task, $subtask, $placeholders = FALSE) {
+  if ($function = ctools_plugin_get_function($task, 'get base contexts')) {
+    return $function($task, $subtask, $placeholders);
+  }
+
+  return array();
+}
+
+/**
  * Get the arguments from a task that are used to load contexts.
  */
 function ctools_context_handler_get_task_arguments($task, $subtask) {
@@ -359,9 +409,6 @@ function ctools_context_handler_edit_context(&$form, &$form_state) {
     '#value' => theme('ctools_context_list', $cache, t('Summary of contexts')),
   );
 
-  // @todo -- this CSS is actually dependent upon the plugins which means
-  // the plugins need to be able to add it.
-  drupal_add_css(panels_get_path('css/panels_admin.css'));
   $form_state['context_object'] = &$cache;
 }
 
index f234a42..76c1cef 100644 (file)
@@ -456,8 +456,8 @@ function _ctools_context_get_converters($id, $plugin_name) {
     $converters = (array) $function();
   }
 
-  foreach (module_implements('ctools_context_convert_list') as $module) {
-    $function = $module . '_ctools_context_convert_list';
+  foreach (module_implements('ctools_context_convert_list_alter') as $module) {
+    $function = $module . '_ctools_context_convert_list_alter';
     $function($plugin, $converters);
   }
 
@@ -608,8 +608,10 @@ function ctools_context_keyword_substitute($string, $keywords, $contexts) {
         continue;
       }
 
+      // Figure out our keyword and converter, if specified.
       if (strpos($keyword, ':')) {
         list($context, $converter) = explode(':', $keyword);
+<<<<<<< HEAD
         if (empty($context->empty) && isset($context_keywords[$context])) {
           $keywords['%' . $keyword] = ctools_context_convert_context($context_keywords[$context], $converter);
         }
@@ -625,8 +627,28 @@ function ctools_context_keyword_substitute($string, $keywords, $contexts) {
           else {
             $keywords['%' . $keyword] = '';
           }
+=======
+      }
+      else {
+        $context = $keyword;
+        $plugin = ctools_get_context($context_keywords[$context]->plugin);
+
+        // Fall back to a default converter, if specified.
+        if ($plugin && !empty($plugin['convert default'])) {
+          $converter = $plugin['convert default'];
+>>>>>>> DRUPAL-6--1
         }
       }
+
+      if (empty($context_keywords[$context]) || !empty($context_keywords[$context]->empty)) {
+        $keywords['%' . $keyword] = '';
+      }
+      else if (!empty($converter)) {
+        $keywords['%' . $keyword] = ctools_context_convert_context($context_keywords[$context], $converter);
+      }
+      else {
+        $keywords['%' . $keyword] = $context_keywords[$keyword]->title;
+      }
     }
   }
 
@@ -845,20 +867,24 @@ function ctools_get_relationships() {
  *   - identifier: The human readable identifier for this relationship, usually
  *     defined by the UI.
  *   - keyword: The keyword used for this relationship for substitutions.
+ *
  * @param $source_context
  *   The context this relationship is based upon.
  *
+ * @param $placeholders
+ *   If TRUE, placeholders are acceptable.
+ *
  * @return
  *   A context object if one can be loaded.
  */
-function ctools_context_get_context_from_relationship($relationship, $source_context) {
+function ctools_context_get_context_from_relationship($relationship, $source_context, $placeholders = FALSE) {
   ctools_include('plugins');
   if ($function = ctools_plugin_load_function('ctools', 'relationships', $relationship['name'], 'context')) {
     if (!isset($relationship['relationship_settings'])) {
       $relationship['relationship_settings'] = array();
     }
 
-    $context = $function($source_context, $relationship['relationship_settings']);
+    $context = $function($source_context, $relationship['relationship_settings'], $placeholders);
     if ($context) {
       $context->identifier = $relationship['identifier'];
       $context->page_title = isset($relationship['title']) ? $relationship['title'] : '';
@@ -913,11 +939,15 @@ function ctools_context_get_relevant_relationships($contexts) {
  *   - context: context id relationship belongs to. This will be used to
  *     identify which context in the $contexts array to use to create the
  *     relationship context.
+ *
  * @param $contexts
  *   A keyed array of contexts used to figure out which relationships
  *   are relevant. New contexts will be added to this.
+ *
+ * @param $placeholders
+ *   If TRUE, placeholders are acceptable.
  */
-function ctools_context_get_context_from_relationships($relationships, &$contexts) {
+function ctools_context_get_context_from_relationships($relationships, &$contexts, $placeholders = FALSE) {
   $return = array();
 
   foreach ($relationships as $rdata) {
@@ -1034,12 +1064,17 @@ function ctools_context_get_context_from_context($context, $type = 'context', $a
  * @param $type
  *   Either 'context' or 'requiredcontext', which indicates whether the contexts
  *   are loaded from internal data or copied from an external source.
+ * @param $placeholders
+ *   If true, placeholders are acceptable.
  */
-function ctools_context_get_context_from_contexts($contexts, $type = 'context') {
+function ctools_context_get_context_from_contexts($contexts, $type = 'context', $placeholders = FALSE) {
   $return = array();
   foreach ($contexts as $context) {
     $ctext = ctools_context_get_context_from_context($context, $type);
     if ($ctext) {
+      if ($placeholders) {
+        $ctext->placeholder = TRUE;
+      }
       $return[ctools_context_id($context, $type)] = $ctext;
     }
   }
@@ -1068,7 +1103,11 @@ function ctools_context_match_required_contexts($required, $contexts) {
   }
 
   foreach ($required as $r) {
-    $return[ctools_context_id($r, 'requiredcontext')] = array_shift($contexts);
+    $context = drupal_clone(array_shift($contexts));
+    $context->identifier = $r['identifier'];
+    $context->page_title = isset($r['title']) ? $r['title'] : '';
+    $context->keyword    = $r['keyword'];
+    $return[ctools_context_id($r, 'requiredcontext')] = $context;
   }
 
   return $return;
@@ -1106,12 +1145,16 @@ function ctools_context_match_required_contexts($required, $contexts) {
  *   An array of pre-existing contexts that will be part of the return value.
  */
 function ctools_context_load_contexts($object, $placeholders = TRUE, $contexts = array()) {
+  if (!empty($object->base_contexts)) {
+    $contexts += $object->base_contexts;
+  }
+
   if ($placeholders) {
     // This will load empty contexts as placeholders for arguments that come
     // from external sources. If this isn't set, it's assumed these context
     // will already have been matched up and loaded.
     if (!empty($object->requiredcontexts) && is_array($object->requiredcontexts)) {
-      $contexts += ctools_context_get_context_from_contexts($object->requiredcontexts, 'requiredcontext');
+      $contexts += ctools_context_get_context_from_contexts($object->requiredcontexts, 'requiredcontext', $placeholders);
     }
 
     if (!empty($object->arguments) && is_array($object->arguments)) {
@@ -1120,12 +1163,12 @@ function ctools_context_load_contexts($object, $placeholders = TRUE, $contexts =
   }
 
   if (!empty($object->contexts) && is_array($object->contexts)) {
-    $contexts += ctools_context_get_context_from_contexts($object->contexts);
+    $contexts += ctools_context_get_context_from_contexts($object->contexts, 'context', $placeholders);
   }
 
   // add contexts from relationships
   if (!empty($object->relationships) && is_array($object->relationships)) {
-    ctools_context_get_context_from_relationships($object->relationships, $contexts);
+    ctools_context_get_context_from_relationships($object->relationships, $contexts, $placeholders);
   }
 
   return $contexts;
@@ -1141,8 +1184,10 @@ function ctools_context_load_contexts($object, $placeholders = TRUE, $contexts =
  */
 function ctools_context_get_form($contexts) {
   if (!empty($contexts)) {
-    foreach ($contexts as $context) {
-      if (!empty($context->form_id)) {
+    foreach ($contexts as $id => $context) {
+      // if a form shows its id as being a 'required context' that means the
+      // the context is external to this display and does not count.
+      if (!empty($context->form_id) && substr($id, 0, 15) != 'requiredcontext') {
         return $context;
       }
     }
@@ -1325,8 +1370,13 @@ function ctools_access_summary($plugin, $contexts, $test) {
   if ($function = ctools_plugin_get_function($plugin, 'summary')) {
     $required_context = isset($plugin['required context']) ? $plugin['required context'] : array();
     $context          = isset($test['context']) ? $test['context'] : array();
-    $description      = $function($test['settings'], ctools_context_select($contexts, $required_context, $context));
+    $description      = $function($test['settings'], ctools_context_select($contexts, $required_context, $context), $plugin);
+  }
+
+  if (!empty($test['not'])) {
+    $description = "NOT ($description)";
   }
+
   return $description;
 }
 
@@ -1344,7 +1394,7 @@ function ctools_access_group_summary($access, $contexts) {
     $descriptions[] = ctools_access_summary($plugin, $contexts, $test);
   }
 
-  $separator = $access['logic'] == 'AND' ? t(', and ') : t(', or ');
+  $separator = $access['logic'] == 'and' ? t(', and ') : t(', or ');
   return implode($separator, $descriptions);
 }
 
@@ -1387,7 +1437,10 @@ function ctools_access($settings, $contexts = array()) {
         $test_contexts = ctools_context_select($contexts, $required_context, $context);
       }
 
-      $pass = $function($test['settings'], $test_contexts);
+      $pass = $function($test['settings'], $test_contexts, $plugin);
+      if (!empty($test['not'])) {
+        $pass = !$pass;
+      }
     }
 
     if ($pass && $settings['logic'] == 'or') {
index 4caedca..313af97 100644 (file)
@@ -133,7 +133,7 @@ function theme_ctools_context_list($object, $header = '') {
       if (isset($context->keyword)) {
         $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context->keyword));
         foreach (ctools_context_get_converters('%' . $context->keyword . ':', $context) as $keyword => $title) {
-          $desc .= '<br />' . t('@keyword --&gt; @title', array('@context' => $keyword, '@title' => $title));
+          $desc .= '<br />' . t('@keyword --&gt; @title', array('@keyword' => $keyword, '@title' => $title));
         }
         $desc .= '</div>';
 
@@ -144,7 +144,6 @@ function theme_ctools_context_list($object, $header = '') {
       $output .= '<td>' . $desc . '</td>';
       $output .= '</tr>';
       $titles[$id] = $context->identifier;
-      $count++;
     }
   }
 
index 4b0ff2a..6e852de 100644 (file)
@@ -44,7 +44,7 @@
  * Then later on:
  * @code
  *   $filename = ctools_css_retrieve($id);
- *   drupal_add_css($filename);
+ *   ctools_css_add_css($filename);
  * @endcode
  *
  * The CSS that was generated will be stored in the database, so even if the
@@ -136,13 +136,12 @@ function ctools_css_clear($id) {
     ->execute();
 }
 
-
 /**
  * Write a chunk of CSS to a temporary cache file and return the file name.
  *
  * This function optionally filters the CSS (always compressed, if so) and
  * generates a unique filename based upon md5. It returns that filename that
- * can be used with drupal_add_css. Note that as a cache file, technically
+ * can be used with ctools_css_add_css(). Note that as a cache file, technically
  * this file is volatile so it should be checked before it is used to ensure
  * that it exists.
  *
@@ -182,6 +181,38 @@ function ctools_css_cache($css, $filter = TRUE) {
 }
 
 /**
+ * Add a CTools CSS file to the page.
+ *
+ * Because drupal_add_css() does not handle files that it cannot stat, it
+ * can't add files that are stored in a private file system. This will
+ * will check to see if we're using the private file system and use
+ * drupal_set_html_head() instead if that is the case.
+ *
+ * Sadly that will preclude aggregation of any sort, but there doesn't seem to
+ * be any ways around that. Also it will screw with stylesheet order. Again,
+ * sorry.
+ */
+function ctools_css_add_css($filename = NULL, $type = 'module', $media = 'all', $preprocess = TRUE) {
+  switch (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC)) {
+    case FILE_DOWNLOADS_PUBLIC:
+      drupal_add_css($filename, $type, $media, $preprocess);
+      break;
+    case FILE_DOWNLOADS_PRIVATE:
+      $path = file_create_path($filename);
+      if ($path) {
+        $url = file_create_url($path);
+      }
+      else {
+        $url = $filename;
+      }
+
+      $output = '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . $url . '" />'."\n";
+      drupal_set_html_head($output);
+  }
+
+}
+
+/**
  * Filter a chunk of CSS text.
  *
  * This function disassembles the CSS into a raw format that makes it easier
@@ -221,7 +252,7 @@ function ctools_css_assemble($css_data) {
   // Iterate through all the statements.
   foreach ($css_data as $selector_str => $declaration) {
     // Add the selectors, separating them with commas and line feeds.
-    $css .= strpos($selector_str, ',') === FALSE ? $selector_str : preg_replace(", ", ",\n", $selector_str);
+    $css .= strpos($selector_str, ',') === FALSE ? $selector_str : str_replace(", ", ",\n", $selector_str);
     // Add the opening curly brace.
     $css .= " {\n";
     // Iterate through all the declarations.
@@ -482,6 +513,7 @@ function ctools_css_filter_default_allowed_properties() {
     'text-align',
     'text-decoration',
     'text-indent',
+    'text-transform',
     'unicode-bidi',
     'vertical-align',
     'voice-family',
@@ -513,6 +545,7 @@ function ctools_css_filter_default_allowed_values() {
     'both',
     'bottom',
     'brown',
+    'capitalize',
     'center',
     'collapse',
     'dashed',
@@ -521,8 +554,10 @@ function ctools_css_filter_default_allowed_values() {
     'gray',
     'green',
     'italic',
+    'inherit',
     'left',
     'lime',
+    'lowercase',
     'maroon',
     'medium',
     'navy',
@@ -539,6 +574,7 @@ function ctools_css_filter_default_allowed_values() {
     'top',
     'transparent',
     'underline',
+    'uppercase',
     'white',
     'yellow',
   );
index 35d3bf5..7f20537 100644 (file)
@@ -19,6 +19,259 @@ define('EXPORT_IN_DATABASE', 0x01);
 define('EXPORT_IN_CODE', 0x02);
 
 /**
+ * @defgroup export_crud CRUD functions for export.
+ * @{
+ * export.inc supports a small number of CRUD functions that should always
+ * work for every exportable object, no matter how complicated. These
+ * functions allow complex objects to provide their own callbacks, but
+ * in most cases, the default callbacks will be used.
+ *
+ * Note that defaults are NOT set in the $schema because it is presumed
+ * that a module's personalized CRUD functions will already know which
+ * $table to use and not want to clutter up the arguments with it.
+ */
+
+/**
+ * Create a new object for the given $table.
+ *
+ * @param $table
+ *   The name of the table to use to retrieve $schema values. This table
+ *   must have an 'export' section containing data or this function
+ *   will fail.
+ * @param $set_defaults
+ *   If TRUE, which is the default, then default values will be retrieved
+ *   from schema fields and set on the object.
+ *
+ * @return
+ *   The loaded object.
+ */
+function ctools_export_crud_new($table, $set_defaults = TRUE) {
+  $schema = ctools_export_get_schema($table);
+  $export = $schema['export'];
+
+  if (!empty($export['create callback']) && function_exists($export['create callback'])) {
+    return $export['create callback']($set_defaults);
+  }
+  else {
+    return ctools_export_new_object($table, $set_defaults);
+  }
+}
+
+/**
+ * Load a single exportable object.
+ *
+ * @param $table
+ *   The name of the table to use to retrieve $schema values. This table
+ *   must have an 'export' section containing data or this function
+ *   will fail.
+ * @param $name
+ *   The unique ID to load. The field for this ID will be specified by
+ *   the export key, which normally defaults to 'name'.
+ *
+ * @return
+ *   The loaded object.
+ */
+function ctools_export_crud_load($table, $name) {
+  $schema = ctools_export_get_schema($table);
+  $export = $schema['export'];
+
+  if (!empty($export['load callback']) && function_exists($export['load callback'])) {
+    return $export['load callback']($name);
+  }
+  else {
+    $result = ctools_export_load_object($table, 'names', array($name));
+    if (isset($result[$name])) {
+      return $result[$name];
+    }
+  }
+}
+
+/**
+ * Load all exportable objects of a given type.
+ *
+ * @param $table
+ *   The name of the table to use to retrieve $schema values. This table
+ *   must have an 'export' section containing data or this function
+ *   will fail.
+ * @param $reset
+ *   If TRUE, the static cache of all objects will be flushed prior to
+ *   loading all. This can be important on listing pages where items
+ *   might have changed on the page load.
+ * @return
+ *   An array of all loaded objects, keyed by the unique IDs of the export key.
+ */
+function ctools_export_crud_load_all($table, $reset = FALSE) {
+  $schema = ctools_export_get_schema($table);
+  if (empty($schema['export'])) {
+    return array();
+  }
+
+  $export = $schema['export'];
+
+  if ($reset) {
+    ctools_export_load_object_reset($table);
+  }
+
+  if (!empty($export['load all callback']) && function_exists($export['load all callback'])) {
+    return $export['load all callback']($reset);
+  }
+  else {
+    return ctools_export_load_object($table, 'all');
+  }
+}
+
+/**
+ * Save a single exportable object.
+ *
+ * @param $table
+ *   The name of the table to use to retrieve $schema values. This table
+ *   must have an 'export' section containing data or this function
+ *   will fail.
+ * @param $object
+ *   The fully populated object to save.
+ *
+ * @return
+ *   Failure to write a record will return FALSE. Otherwise SAVED_NEW or
+ *   SAVED_UPDATED is returned depending on the operation performed. The
+ *   $object parameter contains values for any serial fields defined by the $table
+ */
+function ctools_export_crud_save($table, &$object) {
+  $schema = ctools_export_get_schema($table);
+  $export = $schema['export'];
+
+  if (!empty($export['save callback']) && function_exists($export['save callback'])) {
+    return $export['save callback']($object);
+  }
+  else {
+    // Objects should have a serial primary key. If not, simply fail to write.
+    if (empty($export['primary key'])) {
+      return FALSE;
+    }
+
+    $key = $export['primary key'];
+    if ($object->export_type & EXPORT_IN_DATABASE) {
+      // Existing record.
+      $update = array($key);
+    }
+    else {
+      // New record.
+      $update = array();
+      $object->export_type = EXPORT_IN_DATABASE;
+    }
+    return drupal_write_record($table, $object, $update);
+  }
+}
+
+/**
+ * Delete a single exportable object.
+ *
+ * This only deletes from the database, which means that if an item is in
+ * code, then this is actually a revert.
+ *
+ * @param $table
+ *   The name of the table to use to retrieve $schema values. This table
+ *   must have an 'export' section containing data or this function
+ *   will fail.
+ * @param $object
+ *   The fully populated object to delete, or the export key.
+ */
+function ctools_export_crud_delete($table, $object) {
+  $schema = ctools_export_get_schema($table);
+  $export = $schema['export'];
+
+  if (!empty($export['delete callback']) && function_exists($export['delete callback'])) {
+    return $export['delete callback']($object);
+  }
+  else {
+    // If we were sent an object, get the export key from it. Otherwise
+    // assume we were sent the export key.
+    $value = is_object($object) ? $object->{$export['key']} : $object;
+    db_query("DELETE FROM {" . $table . "} WHERE " . $export['key'] . " = '%s'", $value);
+  }
+}
+
+/**
+ * Get the exported code of a single exportable object.
+ *
+ * @param $table
+ *   The name of the table to use to retrieve $schema values. This table
+ *   must have an 'export' section containing data or this function
+ *   will fail.
+ * @param $object
+ *   The fully populated object to delete, or the export key.
+ * @param $indent
+ *   Any indentation to apply to the code, in case this object is embedded
+ *   into another, for example.
+ *
+ * @return
+ *   A string containing the executable export of the object.
+ */
+function ctools_export_crud_export($table, $object, $indent = '') {
+  $schema = ctools_export_get_schema($table);
+  $export = $schema['export'];
+
+  if (!empty($export['export callback']) && function_exists($export['export callback'])) {
+    return $export['export callback']($object, $indent);
+  }
+  else {
+    return ctools_export_object($table, $object, $indent);
+  }
+}
+
+/**
+ * Turn exported code into an object.
+ *
+ * Note: If the code is poorly formed, this could crash and there is no
+ * way to prevent this.
+ *
+ * @param $table
+ *   The name of the table to use to retrieve $schema values. This table
+ *   must have an 'export' section containing data or this function
+ *   will fail.
+ * @param $code
+ *   The code to eval to create the object.
+ *
+ * @return
+ *   An object created from the export. This object will NOT have been saved
+ *   to the database. In the case of failure, a string containing all errors
+ *   that the system was able to determine.
+ */
+function ctools_export_crud_import($table, $code) {
+  $schema = ctools_export_get_schema($table);
+  $export = $schema['export'];
+
+  if (!empty($export['import callback']) && function_exists($export['import callback'])) {
+    return $export['import callback']($code);
+  }
+  else {
+    ob_start();
+    eval($code);
+    ob_end_clean();
+
+    if (empty(${$export['identifier']})) {
+      $errors = ob_get_contents();
+      if (empty($errors)) {
+        $errors = t('No item found.');
+      }
+      return $errors;
+    }
+
+    $item = ${$export['identifier']};
+
+    // Set these defaults just the same way that ctools_export_new_object sets
+    // them.
+    $item->export_type = NULL;
+    $item->type = t('Local');
+
+    return $item;
+  }
+}
+
+/**
+ * @}
+ */
+
+/**
  * Load some number of exportable objects.
  *
  * This function will cache the objects, load subsidiary objects if necessary,
@@ -46,6 +299,11 @@ function ctools_export_load_object($table, $type = 'all', $args = array()) {
   $cached_database = &drupal_static('ctools_export_load_object_all');
 
   $schema = ctools_export_get_schema($table);
+  if (empty($schema)) {
+    return array();
+  }
+
+  $join_schemas = array();
   $export = $schema['export'];
 
   if (!isset($cache[$table])) {
@@ -76,12 +334,36 @@ function ctools_export_load_object($table, $type = 'all', $args = array()) {
 
   // Build the query
   $query = db_select($table)->fields($table);
+  // FIXME !!!!! broken after latest merge
+  $alias_count = 1;
+  if (!empty($schema['join'])) {
+    foreach ($schema['join'] as $join_key => $join) {
+      $join_schema = drupal_get_schema($join['table']);
+      if (!empty($join_schema)) {
+        $query .= ' INNER JOIN {' . $join['table'] . '} t__' . $alias_count . ' ON t__0.' . $join['left_key'] . ' = ' . 't__' . $alias_count . '.' . $join['right_key'];
+        $alias_count++;
+        $join_schemas[$join['table']] = $join_schema;
+        if (!empty($join['extra'])) {
+          $query .= ' ' . $join['extra'];
+        }
+      }
+    }
+  }
+
   $conditions = array();
   $query_args = array();
 
   // If they passed in names, add them to the query.
   if ($type == 'names') {
     $query->condition($export['key'], $args, 'IN');
+    // FIXME !!!!! broken after latest merge
+    if (!isset($export['key in table'])) {
+      $conditions[] = "$export[key] IN (" . db_placeholders($args, $schema['fields'][$export['key']]['type']) . ")";
+    }
+    else {
+      $conditions[] = "$export[key] IN (" . db_placeholders($args, $join_schemas[$export['key in table']]['fields'][$export['key']]['type']) . ")";
+    }
+    $query_args = $args;
   }
   else if ($type == 'conditions') {
     foreach ($args as $key => $value) {
@@ -184,14 +466,19 @@ function ctools_export_load_object($table, $type = 'all', $args = array()) {
  */
 function ctools_export_load_object_reset($table = NULL) {
   if (empty($table)) {
-    drupal_static_reset('ctools_export_load_object');
-    drupal_static_reset('ctools_export_load_object_all');
+    ctools_static_reset('ctools_export_load_object');
+    ctools_static_reset('ctools_export_load_object_all');
+    ctools_static_reset('_ctools_export_get_defaults');
+    ctools_static_reset('ctools_plugin_api_info');
   }
   else {
-    $cache = &drupal_static('ctools_export_load_object');
-    $cached_database = &drupal_static('ctools_export_load_object_all');
+    $cache = &ctools_static('ctools_export_load_object');
+    $cached_database = &ctools_static('ctools_export_load_object_all');
+    $cached_defaults = &ctools_static('_ctools_export_get_defaults');
     unset($cache[$table]);
     unset($cached_database[$table]);
+    unset($cached_defaults[$table]);
+    ctools_static_reset('ctools_plugin_api_info');
   }
 }
 
@@ -240,7 +527,7 @@ function ctools_get_default_object($table, $name) {
  * to get default objects.
  */
 function _ctools_export_get_defaults($table, $export) {
-  static $cache = array();
+  $cache = &ctools_static(__FUNCTION__, array());
 
   if (!isset($cache[$table])) {
     $cache[$table] = array();
@@ -259,11 +546,14 @@ function _ctools_export_get_defaults($table, $export) {
       foreach ($modules as $module) {
         $function = $module . '_' . $export['default hook'];
         if (function_exists($function)) {
-          if (empty($export['api'])) {
-            $cache[$table] += (array) $function($export);
-          }
-          else {
-            foreach ((array) $function($export) as $name => $object) {
+          foreach ((array) $function($export) as $name => $object) {
+            // Record the module that provides this exportable.
+            $object->export_module = $module;
+
+            if (empty($export['api'])) {
+              $cache[$table][$name] = $object;
+            }
+            else {
               // If version checking is enabled, ensure that the object can be used.
               if (isset($object->api_version) &&
                 $object->api_version >= $export['api']['minimum_version'] &&
@@ -308,6 +598,18 @@ function _ctools_export_unpack_object($schema, $data, $object = 'stdClass') {
     $object->$field = empty($info['serialize']) ? $data->$field : unserialize($data->$field);
   }
 
+  if (isset($schema['join'])) {
+    foreach ($schema['join'] as $join_key => $join) {
+      $join_schema = ctools_export_get_schema($join['table']);
+      if (!empty($join['load'])) {
+        foreach ($join['load'] as $field) {
+          $info = $join_schema['fields'][$field];
+          $object->$field = empty($info['serialize']) ? $data->$field : unserialize(db_decode_blob($data->$field));
+        }
+      }
+    }
+  }
+
   return $object;
 }
 
@@ -340,9 +642,9 @@ function ctools_var_export($var, $prefix = '') {
     else {
       $output = "array(\n";
       foreach ($var as $key => $value) {
-        $output .= "  '$key' => " . ctools_var_export($value, '  ') . ",\n";
+        $output .= $prefix . "  " . ctools_var_export($key) . " => " . ctools_var_export($value, $prefix . '  ') . ",\n";
       }
-      $output .= ')';
+      $output .= $prefix . ')';
     }
   }
   else if (is_object($var) && get_class($var) === 'stdClass') {
@@ -359,10 +661,6 @@ function ctools_var_export($var, $prefix = '') {
     $output = var_export($var, TRUE);
   }
 
-  if ($prefix) {
-    $output = str_replace("\n", "\n$prefix", $output);
-  }
-
   return $output;
 }
 
@@ -389,8 +687,19 @@ function ctools_export_object($table, $object, $indent = '', $identifier = NULL,
     $output .= $indent . '$' . $identifier . '->' . $field . ' = ' . ctools_var_export($value, $indent) . ";\n";
   }
 
-  // Go through our schema and build correlations.
-  foreach ($schema['fields'] as $field => $info) {
+  $fields = $schema['fields'];
+  if (!empty($schema['join'])) {
+    foreach ($schema['join'] as $join) {
+      if (!empty($join['load'])) {
+        foreach ($join['load'] as $join_field) {
+          $fields[$join_field] = $join['fields'][$join_field];
+        }
+      }
+    }
+  }
+
+  // Go through our schema and joined tables and build correlations.
+  foreach ($fields as $field => $info) {
     if (!empty($info['no export'])) {
       continue;
     }
@@ -403,6 +712,7 @@ function ctools_export_object($table, $object, $indent = '', $identifier = NULL,
       }
     }
 
+    // Note: This is the *field* export callback, not the table one!
     if (!empty($info['export callback']) && function_exists($info['export callback'])) {
       $output .= $indent . '$' . $identifier . '->' . $field . ' = ' . $info['export callback']($object, $field, $value, $indent) . ";\n";
     }
@@ -431,27 +741,59 @@ function ctools_export_object($table, $object, $indent = '', $identifier = NULL,
  * that it's easily available.
  */
 function ctools_export_get_schema($table) {
-  $schema = drupal_get_schema($table);
-
-  if (!isset($schema['export'])) {
-    $schema['export'] = array();
-  }
-
-  // Add some defaults
-  $schema['export'] += array(
-    'key' => 'name',
-    'object' => 'stdClass',
-    'status' => 'default_' . $table,
-    'default hook' => 'default_' . $table,
-    'can disable' => TRUE,
-    'identifier' => $table,
-    'bulk export' => TRUE,
-    'export callback' => "$schema[module]_export_{$table}",
-    'list callback' => "$schema[module]_{$table}_list",
-    'to hook code callback' => "$schema[module]_{$table}_to_hook_code",
-  );
+  $cache = &ctools_static(__FUNCTION__);
+  if (empty($cache[$table])) {
+    $schema = drupal_get_schema($table);
+
+    if (!isset($schema['export'])) {
+      return array();
+    }
 
-  return $schema;
+    if (empty($schema['module'])) {
+      return array();
+    }
+
+    // Add some defaults
+    $schema['export'] += array(
+      'key' => 'name',
+      'key name' => 'Name',
+      'object' => 'stdClass',
+      'status' => 'default_' . $table,
+      'default hook' => 'default_' . $table,
+      'can disable' => TRUE,
+      'identifier' => $table,
+      'primary key' => !empty($schema['primary key']) ? $schema['primary key'][0] : '',
+      'bulk export' => TRUE,
+      'list callback' => "$schema[module]_{$table}_list",
+      'to hook code callback' => "$schema[module]_{$table}_to_hook_code",
+    );
+
+    // If the export definition doesn't have the "primary key" then the CRUD
+    // save callback won't work.
+    if (empty($schema['export']['primary key']) && user_access('administer site configuration')) {
+      drupal_set_message(t('The export definition of @table is missing the "primary key" property.', array('@table' => $table)), 'error');
+    }
+
+    // Notes:
+    // The following callbacks may be defined to override default behavior
+    // when using CRUD functions:
+    //
+    // create callback
+    // load callback
+    // load all callback
+    // save callback
+    // delete callback
+    // export callback
+    // import callback
+    //
+    // See the appropriate ctools_export_crud function for details on what
+    // arguments these callbacks should accept. Please do not call these
+    // directly, always use the ctools_export_crud_* wrappers to ensure
+    // that default implementations are honored.
+    $cache[$table] = $schema;
+  }
+
+  return $cache[$table];
 }
 
 /**
@@ -473,8 +815,8 @@ function ctools_export_get_schemas($for_export = FALSE) {
   return $for_export ? array_filter($export_tables, '_ctools_export_filter_export_tables') : $export_tables;
 }
 
-function _ctools_export_filter_export_tables($export) {
-  return empty($export['bulk export']);
+function _ctools_export_filter_export_tables($schema) {
+  return !empty($schema['export']['bulk export']);
 }
 
 function ctools_export_get_schemas_by_module($modules = array(), $for_export = FALSE) {
@@ -550,7 +892,8 @@ function ctools_export_form(&$form_state, $code, $title = '') {
  *
  * 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.
+ * That's a little safer to use as it won't cause weird database default
+ * situations.
  */
 function ctools_export_new_object($table, $set_defaults = TRUE) {
   $schema = ctools_export_get_schema($table);
@@ -571,7 +914,9 @@ function ctools_export_new_object($table, $set_defaults = TRUE) {
 
   if ($set_defaults) {
     // Set some defaults so this data always exists.
-    $object->export_type = EXPORT_IN_DATABASE;
+    // We don't set the export_type property here, as this object is not saved
+    // yet. We do give it NULL so we don't generate notices trying to read it.
+    $object->export_type = NULL;
     $object->type = t('Local');
   }
   return $object;
@@ -590,20 +935,7 @@ function ctools_export_to_hook_code(&$code, $table, $names = array(), $name = 'f
   }
   // Otherwise, the following code generates basic hook code
   else {
-    $objects = ctools_export_load_object($table, 'names', $names);
-    if ($objects) {
-      $output = "/**\n";
-      $output .= " * Implements hook_{$export['default hook']}()\n";
-      $output .= " */\n";
-      $output .= "function " . $name . "_{$export['default hook']}() {\n";
-      $output .= "  \${$export['identifier']}s = array();\n\n";
-      foreach ($objects as $object) {
-        $output .= $export['export callback']($object, '  '); // if this function does not exist, better to error out than fail silently
-        $output .= "  \${$export['identifier']}s['" . check_plain($object->$export['key']) . "'] = \${$export['identifier']};\n\n";
-      }
-      $output .= "  return \${$export['identifier']}s;\n";
-      $output .= "}\n";
-    }
+    $output = ctools_export_default_to_hook_code($schema, $table, $names, $name);
   }
 
   if (!empty($output)) {
@@ -625,3 +957,55 @@ function ctools_export_to_hook_code(&$code, $table, $names = array(), $name = 'f
     }
   }
 }
+
+/**
+ * Default function to export objects to code.
+ *
+ * Note that if your module provides a 'to hook code callback' then it will
+ * receive only $names and $name as arguments. Your module is presumed to
+ * already know the rest.
+ */
+function ctools_export_default_to_hook_code($schema, $table, $names, $name) {
+  $export = $schema['export'];
+  $output = '';
+  $objects = ctools_export_load_object($table, 'names', $names);
+  if ($objects) {
+    $output = "/**\n";
+    $output .= " * Implementation of hook_{$export['default hook']}()\n";
+    $output .= " */\n";
+    $output .= "function " . $name . "_{$export['default hook']}() {\n";
+    $output .= "  \${$export['identifier']}s = array();\n\n";
+    foreach ($objects as $object) {
+      $output .= ctools_export_crud_export($table, $object, '  ');
+      $output .= "  \${$export['identifier']}s['" . check_plain($object->$export['key']) . "'] = \${$export['identifier']};\n\n";
+    }
+    $output .= "  return \${$export['identifier']}s;\n";
+    $output .= "}\n";
+  }
+
+  return $output;
+}
+/**
+ * Default function for listing bulk exportable objects.
+ */
+function ctools_export_default_list($table, $schema) {
+  $list = array();
+
+  $items = ctools_export_crud_load_all($table);
+  $export_key = $schema['export']['key'];
+
+  foreach ($items as $item) {
+    // Try a couple of possible obvious title keys:
+    if (!empty($item->admin_title)) {
+      $string = "$item->admin_title (" . $item->$export_key . ")";
+    }
+    elseif (!empty($item->title)) {
+      $string = "$item->title (" . $item->$export_key . ")";
+    }
+    else {
+      $string = $item->$export_key;
+    }
+    $list[$item->$export_key] = check_plain($string);
+  }
+  return $list;
+}
index 3e49007..4179a17 100644 (file)
@@ -36,13 +36,14 @@ function ctools_build_form($form_id, &$form_state) {
     unset($_SESSION['batch_form_state']);
   }
   else {
-    // If the incoming $_POST contains a form_build_id, we'll check the
+    // If the incoming $form_state['input'] contains a form_build_id, we'll check the
     // cache for a copy of the form in question. If it's there, we don't
     // have to rebuild the form to proceed. In addition, if there is stored
     // form_state data from a previous step, we'll retrieve it so it can
     // be passed on to the form processing code.
-    if (isset($_POST['form_id']) && $_POST['form_id'] == $form_id && !empty($_POST['form_build_id'])) {
-      $form = form_get_cache($_POST['form_build_id'], $form_state);
+    if (isset($form_state['input']['form_id']) && $form_state['input']['form_id'] == $form_id && !empty($form_state['input']['form_build_id'])) {
+      $form_build_id = $form_state['input']['form_build_id'];
+      $form = form_get_cache($form_build_id, $form_state);
       if (!empty($form['#no_cache']) || empty($form)) {
         unset($form);
       }
@@ -99,7 +100,6 @@ function ctools_build_form($form_id, &$form_state) {
     // altering the $form_state variable, which is passed into them by
     // reference.
     ctools_process_form($form_id, $form, $form_state);
-
     // If we were told not to redirect, but not told to re-render, return
     // here.
     if (!empty($form_state['executed']) && empty($form_state['rerender'])) {
@@ -126,9 +126,15 @@ function ctools_build_form($form_id, &$form_state) {
   // workflow is NOT complete. We need to construct a fresh copy of
   // the form, passing in the latest $form_state in addition to any
   // other variables passed into drupal_get_form().
+  //
+  // If this function is being used to perform an '#ahah' callback
+  // to rebuild some or all of a ctools wizard form step, be sure that
+  // $form_state['wrapper callback'], $form_state['form_info'],
+  // $form_state['step'], $form_state['no_redirect'], and $form_state['rebuild']
+  // are properly set.
 
   if (!empty($form_state['rebuild']) || !empty($form_state['storage'])) {
-    $form = ctools_rebuild_form($form_id, $form_state, $args);
+    $form = ctools_rebuild_form($form_id, $form_state, $args, $form_build_id);
   }
 
   // If whoever is calling this wants the $form array (so that it can render it
@@ -168,7 +174,21 @@ function ctools_rebuild_form($form_id, &$form_state, $args, $form_build_id = NUL
   $args[0] = &$form_state; // Replaces placeholder.
   // And the form_id.
   array_unshift($args, $form_id);
-  $form = call_user_func_array('drupal_retrieve_form', $args);
+
+  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 {
+    $form = call_user_func_array('drupal_retrieve_form', $args);
+  }
 
   if (!isset($form_build_id)) {
     // We need a new build_id for the new version of the form.
@@ -304,6 +324,10 @@ function ctools_validate_form($form_id, $form, &$form_state) {
     }
   }
 
+  if (!empty($form_state['clicked_button']['#skip validation'])) {
+    return;
+  }
+
   _form_validate($form, $form_state, $form_id);
   $validated_forms[$form_id] = TRUE;
 }
index 7806b23..76b4832 100644 (file)
@@ -212,7 +212,7 @@ function ctools_menu_tree_page_data($item, $menu_name = 'navigation') {
 function ctools_menu_set_trail_parent($path) {
   $current = menu_get_active_trail();
   $keep = array_pop($current);
-  
+
   $trail = ctools_get_menu_trail($path);
   $trail[] = $keep;
 
index 13a3bad..ee1c908 100644 (file)
@@ -48,16 +48,17 @@ function ctools_modal_add_js() {
     return;
   }
 
-  $settings = array('CToolsModal' => array(
-    'closeText' => t('Close Window'),
-    'closeImage' => theme('image', ctools_image_path('icon-close-window.png'), t('Close window'), t('Close window')),
-    'throbber' => theme('image', ctools_image_path('throbber.gif'), t('Loading...'), t('Loading')),
-  ));
+  $settings = array(
+    'CToolsModal' => array(
+      'loadingText' => t('Loading...'),
+      'closeText' => t('Close Window'),
+      'closeImage' => theme('image', ctools_image_path('icon-close-window.png'), t('Close window'), t('Close window')),
+      'throbber' => theme('image', ctools_image_path('throbber.gif'), t('Loading...'), t('Loading')),
+    ),
+  );
 
   drupal_add_js($settings, 'setting');
   drupal_add_js('misc/jquery.form.js');
-  ctools_add_js('dimensions');
-  ctools_add_js('mc');
   ctools_add_js('ajax-responder');
   ctools_add_js('modal');
 
@@ -186,7 +187,6 @@ function ctools_modal_text_button($text, $dest, $alt, $class = '') {
  */
 function ctools_modal_form_wrapper($form_id, &$form_state) {
   ctools_include('form');
-  ctools_include('modal');
   // This won't override settings already in.
   $form_state += array(
     're_render' => FALSE,
@@ -205,7 +205,7 @@ function ctools_modal_form_wrapper($form_id, &$form_state) {
  * Render a form into an AJAX display.
  */
 function ctools_modal_form_render($form_state, $output) {
-  $title = empty($form_state['title']) ? '' : $form_state['title'];
+  $title = empty($form_state['title']) ? drupal_get_title() : $form_state['title'];
 
   // If there are messages for the form, render them.
   if ($messages = theme('status_messages')) {
@@ -232,4 +232,3 @@ function ctools_modal_render($title, $output) {
   $commands[] = ctools_modal_command_display($title, $output);
   ajax_render($commands);
 }
-
index 739ca15..aa4a6b8 100644 (file)
@@ -28,7 +28,7 @@
  *   The data that was cached.
  */
 function ctools_object_cache_get($obj, $name, $skip_cache = FALSE) {
-  static $cache = array();
+  $cache = &drupal_static(__FUNCTION__, array());
   $key = "$obj:$name";
   if ($skip_cache) {
     unset($cache[$key]);
@@ -55,6 +55,13 @@ function ctools_object_cache_get($obj, $name, $skip_cache = FALSE) {
  *   The object to be cached. This will be serialized prior to writing.
  */
 function ctools_object_cache_set($obj, $name, $cache) {
+  // Store the CTools session id in the user session to force a
+  // session for anonymous users in Drupal 7 and Drupal 6 Pressflow.
+  // see http://drupal.org/node/562374, http://drupal.org/node/861778
+  if (empty($GLOBALS['user']->uid) && empty($_SESSION['ctools_session_id'])) {
+    $_SESSION['ctools_hold_session'] = TRUE;
+  }
+
   ctools_object_cache_clear($obj, $name);
   db_insert('ctools_object_cache')
     ->fields(array(
@@ -82,6 +89,9 @@ function ctools_object_cache_clear($obj, $name) {
     ->condition('obj', $obj)
     ->condition('name', $name)
     ->execute();
+  // Ensure the static cache is emptied of this obj:name set.
+  $cache = &drupal_static('ctools_object_cache_get', array());
+  unset($cache["$obj:$name"]);
 }
 
 
@@ -133,6 +143,34 @@ function ctools_object_cache_test_objects($obj, $names) {
 }
 
 /**
+ * Get the cache status of a group of objects.
+ *
+ * This is useful for displaying lock status when listing a number of objects
+ * an an administration UI.
+ *
+ * @param $obj
+ *   A 32 character or less string to define what kind of object is being
+ *   stored; primarily this is used to prevent collisions.
+ * @param $names
+ *   An array of names of objects
+ *
+ * @return
+ *   An array of objects containing the UID and updated date for each name found.
+ */
+function ctools_object_cache_test_objects($obj, $names) {
+  $placeholders = db_placeholders($names, 'varchar');
+  $args = array_merge(array($obj), $names);
+  $result = db_query("SELECT c.name, s.uid, c.updated FROM {ctools_object_cache} c INNER JOIN {sessions}  s ON c.sid = s.sid WHERE c.obj = '%s' AND c.name IN ($placeholders) ORDER BY c.updated ASC", $args);
+
+  $return = array();
+  while ($test = db_fetch_object($result)) {
+    $return[$test->name] = $test;
+  }
+
+  return $return;
+}
+
+/**
  * Remove an object from the non-volatile ctools cache for all session IDs.
  *
  * This is useful for clearing a lock.
@@ -148,6 +186,9 @@ function ctools_object_cache_clear_all($obj, $name) {
     ->condition('obj', $obj)
     ->condition('name', $name)
     ->execute();
+  // Ensure the static cache is emptied of this obj:name set.
+  $cache = &drupal_static('ctools_object_cache_get', array());
+  unset($cache["$obj:$name"]);
 }
 
 /**
index 00e2a09..bcc59d4 100644 (file)
@@ -16,7 +16,7 @@
  * This will ask each module if they support the given API, and if they do
  * it will return an array of information about the modules that do.
  *
- * This function invokes hook_ctools_api. This invokation is statically
+ * This function invokes hook_ctools_api. This invocation is statically
  * cached, so feel free to call it as often per page run as you like, it
  * will cost very little.
  *
@@ -165,12 +165,18 @@ function ctools_plugin_api_include($owner, $api, $minimum_version, $current_vers
 function ctools_get_plugins($module, $type, $id = NULL) {
   // Store local caches of plugins and plugin info so we don't have to do full
   // lookups everytime.
-  static $info = array();
-  static $plugins = array();
+  $info = &ctools_static('ctools_plugin_info', array());
+  $plugins = &ctools_static('ctools_plugins', array());
+
+  // Attempt to shortcut this whole piece of code if we already have
+  // the requested plugin:
+  if ($id && isset($plugins[$module][$type]) && array_key_exists($id, $plugins[$module][$type])) {
+    return $plugins[$module][$type][$id];
+  }
 
   // Store the status of plugin loading. If a module plugin type pair is true,
   // then it is fully loaded and no searching or setup needs to be done.
-  static $setup = array();
+  $setup = &ctools_static('ctools_plugin_setup', array());
 
   // Request metadata/defaults for this plugin from the declaring module. This
   // is done once per page request, upon a request being made for that plugin.
@@ -182,14 +188,13 @@ function ctools_get_plugins($module, $type, $id = NULL) {
 
   // We assume we don't need to build a cache.
   $build_cache = FALSE;
+
   // If the plugin info says this can be cached, check cache first.
   if ($info[$module][$type]['cache'] && empty($setup[$module][$type])) {
-    // @todo Maybe this should use our own table but free wiping
-    // with content updates is convenient.
     $cache = cache_get("plugins:$module:$type", $info[$module][$type]['cache table']);
 
-    // if cache load successful, set $all_hooks and $all_files to true.
     if (!empty($cache->data)) {
+      // Cache load succeeded so use the cached plugin list.
       $plugins[$module][$type]   = $cache->data;
       // Set $setup to true so we know things where loaded.
       $setup[$module][$type]     = TRUE;
@@ -212,8 +217,24 @@ function ctools_get_plugins($module, $type, $id = NULL) {
   if (empty($setup[$module][$type]) && ($build_cache || !$id)) {
     $setup[$module][$type] = TRUE;
     $plugins[$module][$type] = array_merge($plugins[$module][$type], ctools_plugin_load_includes($info[$module][$type]));
+    // If the plugin can have child plugins, and we're loading all plugins,
+    // go through the list of plugins we have and find child plugins.
+    if (!$id && !empty($info[$module][$type]['child plugins'])) {
+      // If a plugin supports children, go through each plugin and ask.
+      $temp = array();
+      foreach ($plugins[$module][$type] as $name => $plugin) {
+        if (!empty($plugin['get children']) && function_exists($plugin['get children'])) {
+          $temp = array_merge($plugin['get children']($plugin, $name), $temp);
+        }
+        else {
+          $temp[$name] = $plugin;
+        }
+      }
+      $plugins[$module][$type] = $temp;
+    }
   }
 
+
   // If we were told earlier that this is cacheable and the cache was
   // empty, give something back.
   if ($build_cache) {
@@ -222,14 +243,32 @@ function ctools_get_plugins($module, $type, $id = NULL) {
 
   // If no id was requested, we are finished here:
   if (!$id) {
-    return $plugins[$module][$type];
+    // Use array_filter because looking for unknown plugins could cause NULL
+    // entries to appear in the list later.
+    return array_filter($plugins[$module][$type]);
   }
 
   // Check to see if we need to look for the file
   if (!array_key_exists($id, $plugins[$module][$type])) {
-    $result = ctools_plugin_load_includes($info[$module][$type], $id);
-    // Set to either what was returned or NULL.
-    $plugins[$module][$type][$id] = isset($result[$id]) ? $result[$id] : NULL;
+    // If we can have child plugins, check to see if the plugin name is in the
+    // format of parent:child and break it up if it is.
+    if (!empty($info[$module][$type]['child plugins']) && strpos($id, ':') !== FALSE) {
+      list($parent, $child) = explode(':', $id, 2);
+    }
+    else {
+      $parent = $id;
+    }
+
+    if (!array_key_exists($parent, $plugins[$module][$type])) {
+      $result = ctools_plugin_load_includes($info[$module][$type], $parent);
+      // Set to either what was returned or NULL.
+      $plugins[$module][$type][$parent] = isset($result[$parent]) ? $result[$parent] : NULL;
+    }
+
+    // If we are looking for a child, and have the parent, ask the parent for the child.
+    if (!empty($child) && !empty($plugins[$module][$type][$parent]) && function_exists($plugins[$module][$type][$parent]['get child'])) {
+      $plugins[$module][$type][$id] = $plugins[$module][$type][$parent]['get child']($plugins[$module][$type][$parent], $parent, $child);
+    }
   }
 
   // At this point we should either have the plugin, or a NULL.
@@ -247,30 +286,52 @@ function ctools_get_plugins($module, $type, $id = NULL) {
  * @return
  *   An array of information created for this plugin.
  */
-function ctools_plugin_load_includes($info, $file = NULL) {
-  // Load all our plugins.
-  $directories = ctools_plugin_get_directories($info);
-  $file_list = array();
-  if (isset($info['extension'])) {
-    $extension = $info['extension'];
-  }
-  else if (isset($info['info file'])) {
-    $extension = 'info';
-  }
-  else {
-    $extension = 'inc';
-  }
+function ctools_plugin_load_includes($info, $filename = NULL) {
+  // Keep a static array so we don't hit drupal_system_listing more than necessary.
+  $all_files = &ctools_static(__FUNCTION__, array());
+
+  // If we're being asked for all plugins of a type, skip any caching
+  // we may have done because this is an admin task and it's ok to
+  // spend the extra time.
+  if (!isset($filename)) {
+    $all_files[$info['module']][$info['type']] = NULL;
+  }
+
+  if (!isset($all_files[$info['module']][$info['type']])) {
+    // If a filename was set, we will try to load our list of files from
+    // cache. This is considered normal operation and we try to reduce
+    // the time spent finding files.
+    if (isset($filename)) {
+      $cache = cache_get("ctools_plugin_files:$info[module]:$info[type]");
+      if ($cache) {
+        $all_files[$info['module']][$info['type']] = $cache->data;
+      }
+    }
+
+    if (!isset($all_files[$info['module']][$info['type']])) {
+      $all_files[$info['module']][$info['type']] = array();
+      // Load all our plugins.
+      $directories = ctools_plugin_get_directories($info);
+      $extension = empty($info['info file']) ? $info['extension'] : 'info';
 
-  foreach ($directories as $module => $path) {
-    $file_list[$module] = drupal_system_listing("/$file." . $extension . '$/', $path, 'name', 0);
+      foreach ($directories as $module => $path) {
+        $all_files[$info['module']][$info['type']][$module] = drupal_system_listing('\.' . $extension . '$', $path, 'name', 0);
+      }
+
+      cache_set("ctools_plugin_files:$info[module]:$info[type]", $all_files[$info['module']][$info['type']]);
+    }
   }
+  $file_list = $all_files[$info['module']][$info['type']];
   $plugins = array();
 
   // Iterate through all the plugin .inc files, load them and process the hook
   // that should now be available.
   foreach (array_filter($file_list) as $module => $files) {
+    if ($filename) {
+      $files = isset($files[$filename]) ? array($filename => $files[$filename]) : array();
+    }
     foreach ($files as $file) {
-      if (isset($info['info file'])) {
+      if (!empty($info['info file'])) {
         // Parse a .info file
         $result = ctools_plugin_process_info($info, $module, $file);
       }
@@ -494,7 +555,7 @@ function ctools_plugin_process($info, $module, $identifier, $path, $file = NULL,
  * more plugins.
  */
 function _ctools_process_data($result, $info, $module, $path, $file) {
-  // Fill in defaults.
+  // Fill in global defaults.
   foreach ($result as $name => $plugin) {
     $result[$name] += array(
       'module' => $module,
@@ -505,11 +566,16 @@ function _ctools_process_data($result, $info, $module, $path, $file) {
       'plugin type' => $info['type'],
     );
 
-    // Fill in plugin specific defaults, if they exist.
+    // Fill in plugin-specific defaults, if they exist.
     if (!empty($info['defaults'])) {
       if (is_array($info['defaults'])) {
         $result[$name] += $info['defaults'];
       }
+      // FIXME This callback-based approach for the 'defaults' key is entirely
+      // redundant with the 'process' callback. Consequently, that approach is
+      // DEPRECATED in favor using a 'process' callback. In the next major
+      // version, 'defaults' callbacks will be removed entirely; only the array
+      // addition 'defaults' approach will be allowed.
       else if (function_exists($info['defaults'])) {
         $info['defaults']($info, $result[$name]);
       }
@@ -525,8 +591,7 @@ function _ctools_process_data($result, $info, $module, $path, $file) {
 
 
 /**
- * Process an info file for plugin information, rather than
- * a hook.
+ * Process an info file for plugin information, rather than a hook.
  */
 function ctools_plugin_process_info($info, $module, $file) {
   $result = drupal_parse_info_file($file->filename);
@@ -554,6 +619,9 @@ function ctools_plugin_get_info($module, $type) {
     'cache table' => 'cache',
     'use hooks' => FALSE,
     'defaults' => array(),
+    'process' => '',
+    'extension' => 'inc',
+    'info file' => FALSE,
     'hook' => $module . '_' . $type,
     'load themes' => FALSE,
   );
@@ -691,10 +759,7 @@ function ctools_plugin_get_class($plugin_definition, $class_name, $abstract = FA
   }
 
   if (class_exists($class) &&
-     (!is_array($plugin_definition[$class_name])
-       || empty($plugin_definition[$class_name]['abstract'])
-       || $abstract)) {
-
+    (empty($plugin_definition[$class_name]['abstract']) || $abstract)) {
     return $class;
   }
 }
@@ -721,3 +786,30 @@ function ctools_plugin_load_class($module, $type, $id, $class_name, $abstract =
   $plugin = ctools_get_plugins($module, $type, $id);
   return ctools_plugin_get_class($plugin, $class_name, $abstract);
 }
+
+/**
+ * Sort callback for sorting plugins naturally.
+ *
+ * Sort first by weight, then by title.
+ */
+function ctools_plugin_sort($a, $b) {
+  if (is_object($a)) {
+    $a = (array) $a;
+  }
+  if (is_object($b)) {
+    $b = (array) $b;
+  }
+
+  if (empty($a['weight'])) {
+    $a['weight'] = 0;
+  }
+
+  if (empty($b['weight'])) {
+    $b['weight'] = 0;
+  }
+
+  if ($a['weight'] == $b['weight']) {
+    return strnatcmp(strtolower($a['title']), strtolower($b['title']));
+  }
+  return ($a['weight'] < $b['weight']) ? -1 : 1;
+}
index f091f17..08accdc 100644 (file)
@@ -87,30 +87,35 @@ function ctools_wizard_multistep_form($form_info, $step, &$form_state) {
   // 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;
+  if (!isset($form_state['rerender'])) {
+    $form_state['rerender'] = FALSE;
+  }
+
   $form_state['no_redirect'] = TRUE;
 
   ctools_include('form');
   $output = ctools_build_form($info['form id'], $form_state);
 
-  if (empty($form_state['executed'])) {
+  if (empty($form_state['executed']) || !empty($form_state['rerender'])) {
+    if (empty($form_state['title']) && !empty($info['title'])) {
+      $form_state['title'] = $info['title'];
+    }
+
     if (!empty($form_state['ajax render'])) {
       // Any include files should already be included by this point:
       return $form_state['ajax render']($form_state, $output);
     }
 
     // Automatically use the modal tool if set to true.
-    if (!empty($form_state['modal'])) {
+    if (!empty($form_state['modal']) && empty($form_state['modal return'])) {
       ctools_include('modal');
-      if (empty($form_state['title'])) {
-        $form_state['title'] = $info['title'];
-      }
 
       // This overwrites any previous commands.
       $form_state['commands'] = ctools_modal_form_render($form_state, $output);
     }
   }
-  else  {
+
+  if (!empty($form_state['executed'])) {
     // We use the plugins get_function format because it's powerful and
     // not limited to just functions.
     ctools_include('plugins');
@@ -124,7 +129,7 @@ function ctools_wizard_multistep_form($form_info, $step, &$form_state) {
       }
 
       // If the modal is in use, some special code for it:
-      if (!empty($form_state['modal'])) {
+      if (!empty($form_state['modal']) && empty($form_state['modal return'])) {
         if ($type != 'next') {
           // Automatically dismiss the modal if we're not going to another form.
           ctools_include('modal');
@@ -195,6 +200,20 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
     }
 
     if (!empty($form_info['show trail'])) {
+      if (!empty($form_info['free trail'])) {
+        // ctools_wizard_get_path() returns results suitable for #redirect
+        // which can only be directly used in drupal_goto. We have to futz
+        // with it.
+        $path = ctools_wizard_get_path($form_info, $id);
+        $options = array();
+        if (!empty($path[1])) {
+          $options['query'] = $path[1];
+        }
+        if (!empty($path[2])) {
+          $options['fragment'] = $path[2];
+        }
+        $title = l($title, $path[0], $options);
+      }
       $trail[] = '<span class="' . $class . '">' . $title . '</span>';
     }
   }
@@ -228,6 +247,7 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
         '#next' => $form_state['previous'],
         '#wizard type' => 'next',
         '#weight' => -2000,
+        '#skip validation' => TRUE,
         // hardcode the submit so that it doesn't try to save data.
         '#submit' => array('ctools_wizard_submit'),
         '#attributes' => $button_attributes,
@@ -239,11 +259,11 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
     }
 
     // If there is a next form, place the next button.
-    if (isset($form_state['next'])) {
+    if (isset($form_state['next']) || !empty($form_info['free trail'])) {
       $form['buttons']['next'] = array(
         '#type' => 'submit',
         '#value' => $form_info['next text'],
-        '#next' => $form_state['next'],
+        '#next' => !empty($form_info['free trail']) ? $form_state['step'] : $form_state['next'],
         '#wizard type' => 'next',
         '#weight' => -1000,
         '#attributes' => $button_attributes,
@@ -266,7 +286,7 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
         '#attributes' => $button_attributes,
       );
     }
-    else if (empty($form_state['next'])) {
+    else if (empty($form_state['next']) || !empty($form_info['free trail'])) {
       $form['buttons']['return'] = array(
         '#type' => 'submit',
         '#value' => $form_info['finish text'],
@@ -282,6 +302,7 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
         '#value' => $form_info['cancel text'],
         '#wizard type' => 'cancel',
         // hardcode the submit so that it doesn't try to save data.
+        '#skip validation' => TRUE,
         '#submit' => array('ctools_wizard_submit'),
         '#attributes' => $button_attributes,
       );
@@ -312,7 +333,21 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
   }
 
   if (!empty($form_state['ajax'])) {
-    $form['#action'] = url(ctools_wizard_get_path($form_state['form_info'], $form_state['step']));
+    $params = ctools_wizard_get_path($form_state['form_info'], $form_state['step']);
+    if (count($params) > 1) {
+      $url = array_shift($params);
+      $options = array();
+
+      $keys = array(0 => 'query', 1 => 'fragment');
+      foreach ($params as $key => $value) {
+        if (isset($keys[$key]) && isset($value)) {
+          $options[$keys[$key]] = $value;
+        }
+      }
+
+      $params = array($url, $options);
+    }
+    $form['#action'] =  call_user_func_array('url', $params);
   }
 
   if (isset($info['wrapper']) && function_exists($info['wrapper'])) {
@@ -358,7 +393,15 @@ function ctools_wizard_submit(&$form, &$form_state) {
  * 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']);
+  if (is_array($form_info['path'])) {
+    foreach ($form_info['path'] as $id => $part) {
+      $form_info['path'][$id] = str_replace('%step', $step, $form_info['path'][$id]);
+    }
+    return $form_info['path'];
+  }
+  else {
+    return array(str_replace('%step', $step, $form_info['path']));
+  }
 }
 
 /**
@@ -371,6 +414,7 @@ function ctools_wizard_defaults(&$form_info) {
   $hook = $form_info['id'];
   $defaults = array(
     'show trail' => FALSE,
+    'free trail' => FALSE,
     'show back' => FALSE,
     'show cancel' => FALSE,
     'show return' => FALSE,
@@ -380,7 +424,13 @@ function ctools_wizard_defaults(&$form_info) {
     'finish text' => t('Finish'),
     'cancel text' => t('Cancel'),
   );
-  $form_info =  $form_info + $defaults;
+
+  if (!empty($form_info['free trail'])) {
+    $defaults['next text'] = t('Update');
+    $defaults['finish text'] = t('Save');
+  }
+
+  $form_info = $form_info + $defaults;
   // set form callbacks if they aren't defined
   foreach($form_info['forms'] as $step => $params) {
     if (!$params['form id']) {
index 17ff3cf..2ae87fa 100644 (file)
@@ -9,6 +9,9 @@
   Drupal.CTools = Drupal.CTools || {};
   Drupal.CTools.AJAX = Drupal.CTools.AJAX || {};
   Drupal.CTools.AJAX.commands = Drupal.CTools.AJAX.commands || {};
+  Drupal.CTools.AJAX.commandCache = Drupal.CTools.AJAX.comandCache || {} ;
+  Drupal.CTools.AJAX.scripts = {};
+  Drupal.CTools.AJAX.css = {};
 
   /**
    * Success callback for an ajax request.
   };
 
   /**
+   * Grab the response from the server and store it.
+   */
+  Drupal.CTools.AJAX.warmCache = function () {
+    // Store this expression for a minor speed improvement.
+    $this = $(this);
+    var old_url = $this.attr('href');
+    // If we are currently fetching, or if we have fetched this already which is
+    // ideal for things like pagers, where the previous page might already have
+    // been seen in the cache.
+    if ($this.hasClass('ctools-fetching') || Drupal.CTools.AJAX.commandCache[old_url]) {
+      return false;
+    }
+
+    // Grab all the links that match this url and add the fetching class.
+    // This allows the caching system to grab each url once and only once
+    // instead of grabbing the url once per <a>.
+    var $objects = $('a[href=' + old_url + ']')
+    $objects.addClass('ctools-fetching');
+    try {
+      url = old_url.replace(/\/nojs(\/|$)/g, '/ajax$1');
+      $.ajax({
+        type: "POST",
+        url: url,
+        data: { 'js': 1, 'ctools_ajax': 1},
+        global: true,
+        success: function (data) {
+          Drupal.CTools.AJAX.commandCache[old_url] = data;
+          $objects.addClass('ctools-cache-warmed').trigger('ctools-cache-warm', [data]);
+        },
+        complete: function() {
+          $objects.removeClass('ctools-fetching');
+        },
+        dataType: 'json'
+      });
+    }
+    catch (err) {
+      $objects.removeClass('ctools-fetching');
+      return false;
+    }
+
+    return false;
+  };
+
+  /**
+   * Cachable click handler to fetch the commands out of the cache or from url.
+   */
+  Drupal.CTools.AJAX.clickAJAXCacheLink = function () {
+    $this = $(this);
+    if ($this.hasClass('ctools-fetching')) {
+      $this.bind('ctools-cache-warm', function (event, data) {
+        Drupal.CTools.AJAX.respond(data);
+      });
+      return false;
+    }
+    else {
+      if ($this.hasClass('ctools-cache-warmed') && Drupal.CTools.AJAX.commandCache[$this.attr('href')]) {
+        Drupal.CTools.AJAX.respond(Drupal.CTools.AJAX.commandCache[$this.attr('href')]);
+        return false;
+      }
+      else {
+        return Drupal.CTools.AJAX.clickAJAXLink.apply(this);
+      }
+    }
+  };
+
+  /**
    * Generic replacement click handler to open the modal with the destination
    * specified by the href of the link.
    */
     var object = $(this);
     $(this).addClass('ctools-ajaxing');
     try {
-      url = url.replace(/nojs/g, 'ajax');
+      url = url.replace(/\/nojs(\/|$)/g, '/ajax$1');
       $.ajax({
         type: "POST",
         url: url,
-        data: { 'js': 1, 'ctools_ajax': 1 },
+        data: { 'js': 1, 'ctools_ajax': 1},
         global: true,
         success: Drupal.CTools.AJAX.respond,
         error: function(xhr) {
           Drupal.CTools.AJAX.handleErrors(xhr, url);
         },
         complete: function() {
-          object.removeClass('ctools-ajaxing');
+          $('.ctools-ajaxing').removeClass('ctools-ajaxing');
         },
         dataType: 'json'
       });
     }
     catch (err) {
       alert("An error occurred while attempting to process " + url);
-      $(this).removeClass('ctools-ajaxing');
+      $('.ctools-ajaxing').removeClass('ctools-ajaxing');
       return false;
     }
+>>>>>>> DRUPAL-6--1
 
     return false;
   };
     var object = $(this);
     try {
       if (url) {
-        url = url.replace('/nojs/', '/ajax/');
+        url = url.replace(/\/nojs(\/|$)/g, '/ajax$1');
         $.ajax({
           type: "POST",
           url: url,
-          data: { 'js': 1, 'ctools_ajax': 1 },
+          data: { 'js': 1, 'ctools_ajax': 1},
           global: true,
           success: Drupal.CTools.AJAX.respond,
           error: function(xhr) {
             Drupal.CTools.AJAX.handleErrors(xhr, url);
           },
           complete: function() {
-            object.removeClass('ctools-ajaxing');
+            $('.ctools-ajaxing').removeClass('ctools-ajaxing');
           },
           dataType: 'json'
         });
       else {
         var form = this.form;
         url = $(form).attr('action');
-        url = url.replace('/nojs/', '/ajax/');
-        $(form).ajaxSubmit({
-          type: "POST",
-          url: url,
-          data: { 'js': 1, 'ctools_ajax': 1 },
-          global: true,
-          success: Drupal.CTools.AJAX.respond,
-          error: function(xhr) {
-            Drupal.CTools.AJAX.handleErrors(xhr, url);
-          },
-          complete: function() {
-            object.removeClass('ctools-ajaxing');
-          },
-          dataType: 'json'
-        });
+        setTimeout(function() { Drupal.CTools.AJAX.ajaxSubmit(form, url); }, 1);
       }
     }
     catch (err) {
   };
 
   /**
+   * Event handler to submit an AJAX form.
+   *
+   * Using a secondary event ensures that our form submission is last, which
+   * is needed when submitting wysiwyg controlled forms, for example.
+   */
+  Drupal.CTools.AJAX.ajaxSubmit = function (form, url) {
+    $form = $(form);
+
+    if ($form.hasClass('ctools-ajaxing')) {
+      return false;
+    }
+
+    $form.addClass('ctools-ajaxing');
+
+    try {
+      url = url.replace(/\/nojs(\/|$)/g, '/ajax$1');
+
+      var ajaxOptions = {
+        type: 'POST',
+        url: url,
+        data: { 'js': 1, 'ctools_ajax': 1},
+        global: true,
+        success: Drupal.CTools.AJAX.respond,
+        error: function(xhr) {
+          Drupal.CTools.AJAX.handleErrors(xhr, url);
+        },
+        complete: function() {
+          $('.ctools-ajaxing').removeClass('ctools-ajaxing');
+          $('div.ctools-ajaxing-temporary').remove();
+        },
+        dataType: 'json'
+      };
+
+      // If the form requires uploads, use an iframe instead and add data to
+      // the submit to support this and use the proper response.
+      if ($form.attr('enctype') == 'multipart/form-data') {
+        $form.append('<input type="hidden" name="ctools_multipart" value="1">');
+        ajaxIframeOptions = {
+          success: Drupal.CTools.AJAX.iFrameJsonRespond,
+          iframe: true
+        };
+        ajaxOptions = $.extend(ajaxOptions, ajaxIframeOptions);
+      }
+
+      $form.ajaxSubmit(ajaxOptions);
+    }
+    catch (err) {
+      alert("An error occurred while attempting to process " + url);
+      $('.ctools-ajaxing').removeClass('ctools-ajaxing');
+      $('div.ctools-ajaxing-temporary').remove();
+      return false;
+    }
+  };
+
+  /**
+   * Wrapper for handling JSON responses from an iframe submission
+   */
+  Drupal.CTools.AJAX.iFrameJsonRespond = function(data) {
+    var myJson = eval(data);
+    Drupal.CTools.AJAX.respond(myJson);
+  }
+
+  /**
    * Display error in a more fashion way
    */
   Drupal.CTools.AJAX.handleErrors = function(xhr, path) {
     var form_id = $(object).parents('form').get(0).id;
     try {
       if (url) {
-        url = url.replace('/nojs/', '/ajax/');
+        url = url.replace(/\/nojs(\/|$)/g, '/ajax$1');
         $.ajax({
           type: "POST",
           url: url,
             Drupal.CTools.AJAX.handleErrors(xhr, url);
           },
           complete: function() {
-            object.removeClass('ctools-ajaxing');
+            $('.ctools-ajaxing').removeClass('ctools-ajaxing');
             if ($(object).hasClass('ctools-ajax-submit-onchange')) {
               $('form#' + form_id).submit();
             }
     }
     catch (err) {
       alert("An error occurred while attempting to process " + url);
-      $(this).removeClass('ctools-ajaxing');
+      $('.ctools-ajaxing').removeClass('ctools-ajaxing');
       return false;
     }
     return false;
     return url;
   };
 
+  Drupal.CTools.AJAX.getPath = function (link) {
+    if (!link) {
+      return;
+    }
+
+    var index = link.indexOf('?');
+    if (index != -1) {
+      link = link.substr(0, index);
+    }
+
+    return link;
+  }
+
   Drupal.CTools.AJAX.commands.prepend = function(data) {
     $(data.selector).prepend(data.data);
     Drupal.attachBehaviors($(data.selector));
     $(data.selector).css(data.argument);
   };
 
+  Drupal.CTools.AJAX.commands.css_files = function(data) {
+    // Build a list of css files already loaded:
+    $('link:not(.ctools-temporary-css)').each(function () {
+      if ($(this).attr('type') == 'text/css') {
+        var link = Drupal.CTools.AJAX.getPath($(this).attr('href'));
+        if (link) {
+          Drupal.CTools.AJAX.css[link] = $(this).attr('href');
+        }
+      }
+    });
+
+    var html = '';
+    for (i in data.argument) {
+      var link = Drupal.CTools.AJAX.getPath(data.argument[i].file);
+      if (!Drupal.CTools.AJAX.css[link]) {
+        html += '<link class="ctools-temporary-css" type="text/css" rel="stylesheet" media="' + data.argument[i].media +
+          '" href="' + data.argument[i].file + '" />';
+      }
+    }
+
+    if (html) {
+      $('link.ctools-temporary-css').remove();
+      $('body').append($(html));
+    }
+  };
+
   Drupal.CTools.AJAX.commands.settings = function(data) {
     $.extend(Drupal.settings, data.argument);
   };
 
+  Drupal.CTools.AJAX.commands.scripts = function(data) {
+    // Build a list of scripts already loaded:
+    var scripts = {};
+    $('script').each(function () {
+      var link = Drupal.CTools.AJAX.getPath($(this).attr('src'));
+      if (link) {
+        Drupal.CTools.AJAX.scripts[link] = $(this).attr('src');
+      }
+    });
+
+    var html = '';
+    var head = document.getElementsByTagName('head')[0];
+    for (i in data.argument) {
+      var link = Drupal.CTools.AJAX.getPath(data.argument[i]);
+      if (!Drupal.CTools.AJAX.scripts[link]) {
+        Drupal.CTools.AJAX.scripts[link] = link;
+        // Use this to actually get the script tag into the dom, which is
+        // needed for scripts that self-reference to determine paths.
+        var script = document.createElement('script');
+        script.type = 'text/javascript';
+        script.src = data.argument[i];
+        head.appendChild(script);
+        html += '<script type="text/javascript" src="' + data.argument[i] + '"></script>';
+      }
+    }
+
+    if (html) {
+      $('body').append($(html));
+    }
+  };
+
   Drupal.CTools.AJAX.commands.data = function(data) {
     $(data.selector).data(data.name, data.value);
   };
   };
 
   Drupal.CTools.AJAX.commands.redirect = function(data) {
-    location.href = data.url;
+    if (data.delay > 0) {
+      setTimeout(function () {
+        location.href = data.url;
+      }, data.delay);
+    }
+    else {
+      location.href = data.url;
+    }
   };
 
   Drupal.CTools.AJAX.commands.reload = function(data) {
    */
   Drupal.behaviors.CToolsAJAX = function(context) {
     // Bind links
+
+    // Note that doing so in this order means that the two classes can be
+    // used together safely.
+    $('a.ctools-use-ajax-cache:not(.ctools-use-ajax-processed)', context)
+      .addClass('ctools-use-ajax-processed')
+      .click(Drupal.CTools.AJAX.clickAJAXCacheLink)
+      .each(function () {
+        Drupal.CTools.AJAX.warmCache.apply(this);
+      });
+
     $('a.ctools-use-ajax:not(.ctools-use-ajax-processed)', context)
       .addClass('ctools-use-ajax-processed')
       .click(Drupal.CTools.AJAX.clickAJAXLink);
        .filter('.ctools-use-ajax-onchange:not(.ctools-use-ajax-processed)')
        .addClass('ctools-use-ajax-processed')
        .change(Drupal.CTools.AJAX.changeAJAX);
+
+    // Add information about loaded CSS and JS files.
+    if (Drupal.settings.CToolsAJAX && Drupal.settings.CToolsAJAX.css) {
+      $.extend(Drupal.CTools.AJAX.css, Drupal.settings.CToolsAJAX.css);
+    }
+    if (Drupal.settings.CToolsAJAX && Drupal.settings.CToolsAJAX.scripts) {
+      $.extend(Drupal.CTools.AJAX.scripts, Drupal.settings.CToolsAJAX.scripts);
+    }
   };
 })(jQuery);
index c49f3cf..690ce4f 100644 (file)
         else {
           content.slideToggle(100, afterToggle);
         }
+
         toggle.toggleClass('ctools-toggle-collapsed');
 
         // If we're supposed to remember the state of this class, do so.
index b0f3214..c35757d 100644 (file)
 
   /**
    * Display the modal
+   *
+   * @todo -- document the settings.
    */
-  Drupal.CTools.Modal.show = function() {
+  Drupal.CTools.Modal.show = function(choice) {
+    var opts = {};
+
+    if (choice && typeof choice == 'string' && Drupal.settings[choice]) {
+      // This notation guarantees we are actually copying it.
+      $.extend(true, opts, Drupal.settings[choice]);
+    }
+    else if (choice) {
+      $.extend(true, opts, choice);
+    }
+
+    var defaults = {
+      modalTheme: 'CToolsModalDialog',
+      throbberTheme: 'CToolsModalThrobber',
+      animation: 'show',
+      animationSpeed: 'fast',
+      modalSize: {
+        type: 'scale',
+        width: .8,
+        height: .8,
+        addWidth: 0,
+        addHeight: 0,
+        // How much to remove from the inner content to make space for the
+        // theming.
+        contentRight: 25,
+        contentBottom: 45
+      },
+      modalOptions: {
+        opacity: .55,
+        background: '#fff'
+      }
+    };
+
+    var settings = {};
+    $.extend(true, settings, defaults, Drupal.settings.CToolsModal, opts);
+
+    if (Drupal.CTools.Modal.currentSettings && Drupal.CTools.Modal.currentSettings != settings) {
+      Drupal.CTools.Modal.modal.remove();
+      Drupal.CTools.Modal.modal = null;
+    }
+
+    Drupal.CTools.Modal.currentSettings = settings;
+
     var resize = function(e) {
-      // For reasons I do not understand, when creating the modal the context must be
-      // Drupal.CTools.Modal.modal but otherwise the context must be more than that.
+      // When creating the modal, it actually exists only in a theoretical
+      // place that is not in the DOM. But once the modal exists, it is in the
+      // DOM so the context must be set appropriately.
       var context = e ? document : Drupal.CTools.Modal.modal;
+
+      if (Drupal.CTools.Modal.currentSettings.modalSize.type == 'scale') {
+        var width = $(window).width() * Drupal.CTools.Modal.currentSettings.modalSize.width;
+        var height = $(window).height() * Drupal.CTools.Modal.currentSettings.modalSize.height;
+      }
+      else {
+        var width = Drupal.CTools.Modal.currentSettings.modalSize.width;
+        var height = Drupal.CTools.Modal.currentSettings.modalSize.height;
+      }
+
+      // Use the additionol pixels for creating the width and height.
       $('div.ctools-modal-content', context).css({
-        'width': $(window).width() * .8 + 'px',
-        'height': $(window).height() * .8 + 'px'
+        'width': width + Drupal.CTools.Modal.currentSettings.modalSize.addWidth + 'px',
+        'height': height + Drupal.CTools.Modal.currentSettings.modalSize.addHeight + 'px'
       });
       $('div.ctools-modal-content .modal-content', context).css({
-        'width': ($(window).width() * .8 - 25) + 'px',
-        'height': ($(window).height() * .8 - 35) + 'px'
+        'width': (width - Drupal.CTools.Modal.currentSettings.modalSize.contentRight) + 'px',
+        'height': (height - Drupal.CTools.Modal.currentSettings.modalSize.contentBottom) + 'px'
       });
     }
 
     if (!Drupal.CTools.Modal.modal) {
-      Drupal.CTools.Modal.modal = $(Drupal.theme('CToolsModalDialog'));
-      $(window).bind('resize', resize);
+      Drupal.CTools.Modal.modal = $(Drupal.theme(settings.modalTheme));
+      if (settings.modalSize.type == 'scale') {
+        $(window).bind('resize', resize);
+      }
     }
 
     resize();
-    $('span.modal-title', Drupal.CTools.Modal.modal).html(Drupal.t('Loading...'));
-    var opts = {
-      // @todo this should be elsewhere.
-      opacity: '.55',
-      background: '#fff'
-    };
 
-    Drupal.CTools.Modal.modalContent(Drupal.CTools.Modal.modal, opts);
-    $('#modalContent .modal-content').html(Drupal.theme('CToolsModalThrobber'));
+    $('span.modal-title', Drupal.CTools.Modal.modal).html(Drupal.CTools.Modal.currentSettings.loadingText);
+    Drupal.CTools.Modal.modalContent(Drupal.CTools.Modal.modal, settings.modalOptions, settings.animation, settings.animationSpeed);
+    $('#modalContent .modal-content').html(Drupal.theme(settings.throbberTheme));
   };
 
   /**
     html += '    <div class="ctools-modal-content">' // panels-modal-content
     html += '      <div class="modal-header">';
     html += '        <a class="close" href="#">';
-    html +=            Drupal.settings.CToolsModal.closeText + Drupal.settings.CToolsModal.closeImage;
+    html +=            Drupal.CTools.Modal.currentSettings.closeText + Drupal.CTools.Modal.currentSettings.closeImage;
     html += '        </a>';
     html += '        <span id="modal-title" class="modal-title">&nbsp;</span>';
     html += '      </div>';
     var html = '';
     html += '  <div id="modal-throbber">';
     html += '    <div class="modal-throbber-wrapper">';
-    html +=        Drupal.settings.CToolsModal.throbber;
+    html +=        Drupal.CTools.Modal.currentSettings.throbber;
     html += '    </div>';
     html += '  </div>';
 
   };
 
   /**
+   * Figure out what settings string to use to display a modal.
+   */
+  Drupal.CTools.Modal.getSettings = function (object) {
+    var match = $(object).attr('class').match(/ctools-modal-(\S+)/);
+    if (match) {
+      return match[1];
+    }
+  }
+
+  /**
+   * Click function for modals that can be cached.
+   */
+  Drupal.CTools.Modal.clickAjaxCacheLink = function () {
+    Drupal.CTools.Modal.show(Drupal.CTools.Modal.getSettings(this));
+    return Drupal.CTools.AJAX.clickAJAXCacheLink.apply(this);
+  };
+
+  /**
    * Generic replacement click handler to open the modal with the destination
    * specified by the href of the link.
    */
-  Drupal.CTools.Modal.clickAjaxLink = function() {
+  Drupal.CTools.Modal.clickAjaxLink = function () {
     // show the empty dialog right away.
-    Drupal.CTools.Modal.show();
+    Drupal.CTools.Modal.show(Drupal.CTools.Modal.getSettings(this));
     Drupal.CTools.AJAX.clickAJAXLink.apply(this);
     if (!$(this).hasClass('ctools-ajaxing')) {
       Drupal.CTools.Modal.dismiss();
       return false;
     }
 
-    Drupal.CTools.Modal.show();
+    Drupal.CTools.Modal.show(Drupal.CTools.Modal.getSettings(this));
     Drupal.CTools.AJAX.clickAJAXButton.apply(this);
     if (!$(this).hasClass('ctools-ajaxing')) {
       Drupal.CTools.Modal.dismiss();
   /**
    * Submit responder to do an AJAX submit on all modal forms.
    */
-  Drupal.CTools.Modal.submitAjaxForm = function() {
-    if ($(this).hasClass('ctools-ajaxing')) {
-      return false;
-    }
+  Drupal.CTools.Modal.submitAjaxForm = function(e) {
+    var url = $(this).attr('action');
+    var form = $(this);
 
-    url = $(this).attr('action');
-    $(this).addClass('ctools-ajaxing');
-    var object = $(this);
-    try {
-      url.replace('/nojs/', '/ajax/');
-
-      var ajaxOptions = {
-        type: 'POST',
-        url: url,
-        data: { 'js': 1, 'ctools_ajax': 1 },
-        global: true,
-        success: Drupal.CTools.AJAX.respond,
-        error: function(xhr) {
-          Drupal.CTools.AJAX.handleErrors(xhr, url);
-        },
-        complete: function() {
-          object.removeClass('ctools-ajaxing');
-          $('.ctools-ajaxing', object).removeClass('ctools-ajaxing');
-        },
-        dataType: 'json'
-      };
-
-      // If the form requires uploads, use an iframe instead and add data to
-      // the submit to support this and use the proper response.
-      if ($(this).attr('enctype') == 'multipart/form-data') {
-        $(this).append('<input type="hidden" name="ctools_multipart" value="1">');
-        ajaxIframeOptions = {
-          success: Drupal.CTools.AJAX.iFrameJsonRespond,
-          iframe: true
-        };
-        ajaxOptions = $.extend(ajaxOptions, ajaxIframeOptions);
-      }
-
-      $(this).ajaxSubmit(ajaxOptions);
-    }
-    catch (err) {
-      alert("An error occurred while attempting to process " + url);
-      $(this).removeClass('ctools-ajaxing');
-      $('div.ctools-ajaxing', this).remove();
-      return false;
-    }
+    setTimeout(function() { Drupal.CTools.AJAX.ajaxSubmit(form, url); }, 1);
     return false;
   }
 
   /**
-   * Wrapper for handling JSON responses from an iframe submission
-   */
-  Drupal.CTools.AJAX.iFrameJsonRespond = function(data) {
-    var myJson = eval(data);
-    Drupal.CTools.AJAX.respond(myJson);
-  }
-
-  /**
    * Bind links that will open modals to the appropriate function.
    */
-  Drupal.behaviors.CToolsModal = function(context) {
+  Drupal.behaviors.ZZCToolsModal = function(context) {
     // Bind links
+    // Note that doing so in this order means that the two classes can be
+    // used together safely.
+    $('a.ctools-use-modal-cache:not(.ctools-use-modal-processed)', context)
+      .addClass('ctools-use-modal-processed')
+      .click(Drupal.CTools.Modal.clickAjaxCacheLink)
+      .each(function () {
+        Drupal.CTools.AJAX.warmCache.apply(this);
+      });
+
     $('a.ctools-use-modal:not(.ctools-use-modal-processed)', context)
       .addClass('ctools-use-modal-processed')
       .click(Drupal.CTools.Modal.clickAjaxLink);
       .addClass('ctools-use-modal-processed')
       .click(Drupal.CTools.Modal.clickAjaxButton);
 
-    if ($(context).attr('id') == 'modal-content') {
-      // Bind submit links in the modal form.
-      $('form:not(.ctools-use-modal-processed)', context)
-        .addClass('ctools-use-modal-processed')
-        .submit(Drupal.CTools.Modal.submitAjaxForm);
-      // add click handlers so that we can tell which button was clicked,
-      // because the AJAX submit does not set the values properly.
-
-      $('input[type="submit"]:not(.ctools-use-modal-processed), button:not(.ctools-use-modal-processed)', context)
-        .addClass('ctools-use-modal-processed')
-        .click(function() {
-          if (Drupal.autocompleteSubmit && !Drupal.autocompleteSubmit()) {
-            return false;
-          }
-
-          // Make sure it knows our button.
-          if (!$(this.form).hasClass('ctools-ajaxing')) {
-            this.form.clk = this;
-            $(this).after('<div class="ctools-ajaxing"> &nbsp; </div>');
-          }
-        });
+    // Bind submit links in the modal form.
+    $('#modal-content form:not(.ctools-use-modal-processed)', context)
+      .addClass('ctools-use-modal-processed')
+      .submit(Drupal.CTools.Modal.submitAjaxForm)
+      .bind('CToolsAJAXSubmit', Drupal.CTools.AJAX.ajaxSubmit);
+
+    // add click handlers so that we can tell which button was clicked,
+    // because the AJAX submit does not set the values properly.
+
+    $('#modal-content input[type="submit"]:not(.ctools-use-modal-processed), #modal-content button:not(.ctools-use-modal-processed)', context)
+      .addClass('ctools-use-modal-processed')
+      .click(function() {
+        if (Drupal.autocompleteSubmit && !Drupal.autocompleteSubmit()) {
+          return false;
+        }
+
+        // Make sure it knows our button.
+        if (!$(this.form).hasClass('ctools-ajaxing')) {
+          this.form.clk = this;
+        }
+      });
 
-    }
   };
 
   // The following are implementations of AJAX responder commands.
   Drupal.CTools.AJAX.commands.modal_display = function(command) {
     $('#modal-title').html(command.title);
     $('#modal-content').html(command.output);
-    Drupal.attachBehaviors($('#modal-content'));
+    Drupal.attachBehaviors();
   }
 
   /**
    */
   Drupal.CTools.AJAX.commands.modal_dismiss = function(command) {
     Drupal.CTools.Modal.dismiss();
+    $('link.ctools-temporary-css').remove();
   }
 
   /**
    */
   Drupal.CTools.AJAX.commands.modal_loading = function(command) {
     Drupal.CTools.AJAX.commands.modal_display({
-      output: Drupal.theme('CToolsModalThrobber'),
-      title: Drupal.t('Loading...')
+      output: Drupal.theme(Drupal.CTools.Modal.currentSettings.throbberTheme),
+      title: Drupal.CTools.Modal.currentSettings.loadingText
     });
   }
+
+  /**
+   * modalContent
+   * @param content string to display in the content box
+   * @param css obj of css attributes
+   * @param animation (fadeIn, slideDown, show)
+   * @param speed (valid animation speeds slow, medium, fast or # in ms)
+   */
+  Drupal.CTools.Modal.modalContent = function(content, css, animation, speed) {
+    // If our animation isn't set, make it just show/pop
+    if (!animation) {
+      animation = 'show';
+    }
+    else {
+      // If our animation isn't "fadeIn" or "slideDown" then it always is show
+      if (animation != 'fadeIn' && animation != 'slideDown') {
+        animation = 'show';
+      }
+    }
+
+    if (!speed) {
+      speed = 'fast';
+    }
+
+    // Build our base attributes and allow them to be overriden
+    css = jQuery.extend({
+      position: 'absolute',
+      left: '0px',
+      margin: '0px',
+      background: '#000',
+      opacity: '.55'
+    }, css);
+
+    // Add opacity handling for IE.
+    css.filter = 'alpha(opacity=' + (100 * css.opacity) + ')';
+    content.hide();
+
+    // if we already ahve a modalContent, remove it
+    if ( $('#modalBackdrop')) $('#modalBackdrop').remove();
+    if ( $('#modalContent')) $('#modalContent').remove();
+
+    // position code lifted from http://www.quirksmode.org/viewport/compatibility.html
+    if (self.pageYOffset) { // all except Explorer
+    var wt = self.pageYOffset;
+    } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict
+      var wt = document.documentElement.scrollTop;
+    } else if (document.body) { // all other Explorers
+      var wt = document.body.scrollTop;
+    }
+
+    // Get our dimensions
+
+    // Get the docHeight and (ugly hack) add 50 pixels to make sure we dont have a *visible* border below our div
+    var docHeight = $(document).height() + 50;
+    var docWidth = $(document).width();
+    var winHeight = $(window).height();
+    var winWidth = $(window).width();
+    if( docHeight < winHeight ) docHeight = winHeight;
+
+    // Create our divs
+    $('body').append('<div id="modalBackdrop" style="z-index: 1000; display: none;"></div><div id="modalContent" style="z-index: 1001; position: absolute;">' + $(content).html() + '</div>');
+
+    // Keyboard and focus event handler ensures focus stays on modal elements only
+    modalEventHandler = function( event ) {
+      target = null;
+      if ( event ) { //Mozilla
+        target = event.target;
+      } else { //IE
+        event = window.event;
+        target = event.srcElement;
+      }
+      if( $(target).filter('*:visible').parents('#modalContent').size()) {
+        // allow the event only if target is a visible child node of #modalContent
+        return true;
+      }
+      if ( $('#modalContent')) $('#modalContent').get(0).focus();
+      return false;
+    };
+    $('body').bind( 'focus', modalEventHandler );
+    $('body').bind( 'keypress', modalEventHandler );
+
+    // Create our content div, get the dimensions, and hide it
+    var modalContent = $('#modalContent').css('top','-1000px');
+    var mdcTop = wt + ( winHeight / 2 ) - (  modalContent.outerHeight() / 2);
+    var mdcLeft = ( winWidth / 2 ) - ( modalContent.outerWidth() / 2);
+    $('#modalBackdrop').css(css).css('top', 0).css('height', docHeight + 'px').css('width', docWidth + 'px').show();
+    modalContent.css({top: mdcTop + 'px', left: mdcLeft + 'px'}).hide()[animation](speed);
+
+    // Bind a click for closing the modalContent
+    modalContentClose = function(){close(); return false;};
+    $('.close').bind('click', modalContentClose);
+
+    // Close the open modal content and backdrop
+    function close() {
+      // Unbind the events
+      $(window).unbind('resize',  modalContentResize);
+      $('body').unbind( 'focus', modalEventHandler);
+      $('body').unbind( 'keypress', modalEventHandler );
+      $('.close').unbind('click', modalContentClose);
+
+      // Set our animation parameters and use them
+      if ( animation == 'fadeIn' ) animation = 'fadeOut';
+      if ( animation == 'slideDown' ) animation = 'slideUp';
+      if ( animation == 'show' ) animation = 'hide';
+
+      // Close the content
+      modalContent.hide()[animation](speed);
+
+      // Remove the content
+      $('#modalContent').remove();
+      $('#modalBackdrop').remove();
+    };
+
+    // Move and resize the modalBackdrop and modalContent on resize of the window
+     modalContentResize = function(){
+      // Get our heights
+      var docHeight = $(document).height();
+      var docWidth = $(document).width();
+      var winHeight = $(window).height();
+      var winWidth = $(window).width();
+      if( docHeight < winHeight ) docHeight = winHeight;
+
+      // Get where we should move content to
+      var modalContent = $('#modalContent');
+      var mdcTop = ( winHeight / 2 ) - (  modalContent.outerHeight() / 2);
+      var mdcLeft = ( winWidth / 2 ) - ( modalContent.outerWidth() / 2);
+
+      // Apply the changes
+      $('#modalBackdrop').css('height', docHeight + 'px').css('width', docWidth + 'px').show();
+      modalContent.css('top', mdcTop + 'px').css('left', mdcLeft + 'px').show();
+    };
+    $(window).bind('resize', modalContentResize);
+
+    $('#modalContent').focus();
+  };
+
+  /**
+   * unmodalContent
+   * @param content (The jQuery object to remove)
+   * @param animation (fadeOut, slideUp, show)
+   * @param speed (valid animation speeds slow, medium, fast or # in ms)
+   */
+  Drupal.CTools.Modal.unmodalContent = function(content, animation, speed)
+  {
+    // If our animation isn't set, make it just show/pop
+    if (!animation) { var animation = 'show'; } else {
+      // If our animation isn't "fade" then it always is show
+      if (( animation != 'fadeOut' ) && ( animation != 'slideUp')) animation = 'show';
+    }
+    // Set a speed if we dont have one
+    if ( !speed ) var speed = 'fast';
+
+    // Unbind the events we bound
+    $(window).unbind('resize', modalContentResize);
+    $('body').unbind('focus', modalEventHandler);
+    $('body').unbind('keypress', modalEventHandler);
+    $('.close').unbind('click', modalContentClose);
+
+    // jQuery magic loop through the instances and run the animations or removal.
+    content.each(function(){
+      if ( animation == 'fade' ) {
+        $('#modalContent').fadeOut(speed,function(){$('#modalBackdrop').fadeOut(speed, function(){$(this).remove();});$(this).remove();});
+      } else {
+        if ( animation == 'slide' ) {
+          $('#modalContent').slideUp(speed,function(){$('#modalBackdrop').slideUp(speed, function(){$(this).remove();});$(this).remove();});
+        } else {
+          $('#modalContent').remove();$('#modalBackdrop').remove();
+        }
+      }
+    });
+  };
+
 })(jQuery);
index 347eb1d..9a0da0f 100644 (file)
@@ -199,12 +199,14 @@ function page_manager_get_pages($tasks, &$pages, $task_id = NULL) {
         $operations[] = array(
           'title' => t('Enable'),
           'href' => 'admin/build/pages/nojs/enable/' . $task_name,
+          'query' => array('token' => drupal_get_token($task_name)),
         );
       }
       else {
         $operations[] = array(
           'title' => t('Disable'),
           'href' => 'admin/build/pages/nojs/disable/' . $task_name,
+          'query' => array('token' => drupal_get_token($task_name)),
         );
       }
     }
@@ -558,11 +560,14 @@ function page_manager_get_operations($page, $operations = NULL) {
     ),
   );
 
-  $result['actions']['children']['import'] = array(
-    'title' => t('Import variant'),
-    'description' => t('Add a new variant to this page from code exported from another page.'),
-    'form' => 'page_manager_handler_import',
-  );
+  // Restrict variant import to users who can already execute arbitrary PHP
+  if (user_access('use PHP for block visibility')) {
+    $result['actions']['children']['import'] = array(
+      'title' => t('Import variant'),
+      'description' => t('Add a new variant to this page from code exported from another page.'),
+      'form' => 'page_manager_handler_import',
+    );
+  }
 
   if (count($page->handlers) > 1) {
     $result['actions']['children']['rearrange'] = array(
@@ -618,6 +623,7 @@ function page_manager_get_operations($page, $operations = NULL) {
     );
   }
 
+  drupal_alter('page_manager_operations', $result, $page);
   return $result;
 }
 
@@ -728,6 +734,8 @@ function page_manager_get_handler_operations(&$page) {
         ),
       );
     }
+
+    drupal_alter('page_manager_variant_operations', $operations[$id], $handler);
   }
   if (empty($operations)) {
     $operations['empty'] = array(
@@ -987,8 +995,13 @@ function page_manager_edit_page_finish(&$form_state) {
   // If the 'Update and Save' button was selected, write our cache out.
   if (!empty($form_state['clicked_button']['#save'])) {
     page_manager_save_page_cache($form_state['page']);
-    $form_state['page']->changed = FALSE;
-//    $form_state['page'] = page_manager_get_page_cache($form_state['task_name']);
+    page_manager_clear_page_cache($form_state['page']->task_name);
+    $form_state['page'] = page_manager_get_page_cache($form_state['page']->task_name);
+    if (empty($form_state['new trail'])) {
+      // force a rerender to get rid of old form items that may have changed
+      // during save.
+      $form_state['new trail'] = $form_state['trail'];
+    }
   }
   else {
     if (empty($form_state['do not cache'])) {
@@ -1377,6 +1390,9 @@ function page_manager_handler_add_form(&$form, $form_state, $features = array())
         '#type' => 'checkbox',
         '#title' => $feature,
       );
+      if (!empty($form_state['page']->forms) && in_array($feature_id, $form_state['page']->forms)) {
+        $form['features'][$plugin][$feature_id]['#default_value'] = TRUE;
+      }
 
       if ($plugin != 'default') {
         $form['features'][$plugin][$feature_id] += array(
@@ -1398,17 +1414,30 @@ function page_manager_handler_import(&$form, &$form_state) {
     '#description' => t('Enter the name of the new variant.'),
   );
 
-  $form['object'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Paste variant code here'),
-    '#rows' => 15,
-  );
+  if (user_access('use PHP for block visibility')) {
+    $form['object'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Paste variant code here'),
+      '#rows' => 15,
+    );
+  }
+  // Users ordinarily can't get here without the PHP block visibility perm.
+  // In case they somehow do, though, disable the form widget for extra safety.
+  else {
+    $form['shoveoff'] = array(
+      '#value' => '<div>' . t('You do not have sufficient permissions to perform this action.') . '</div>',
+    );
+  }
 }
 
 /**
  * Make sure that an import actually provides a handler.
  */
 function page_manager_handler_import_validate($form, &$form_state) {
+  if (!user_access('use PHP for block visibility')) {
+    form_error($form['shoveoff'], t('You account permissions do not permit you to import.'));
+    return;
+  }
   ob_start();
   eval($form_state['values']['object']);
   ob_end_clean();
@@ -1461,6 +1490,7 @@ function page_manager_handler_rearrange(&$form, &$form_state) {
     $form['handlers'][$id]['weight'] = array(
       '#type' => 'weight',
       '#default_value' => $info['weight'],
+      '#delta' => 30,
     );
   }
 }
@@ -1753,6 +1783,9 @@ function page_manager_page_summary(&$form, &$form_state) {
  * Menu callback to enable or disable a page
  */
 function page_manager_enable_page($disable, $js, $page) {
+  if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], $page->task_name)) {
+    return MENU_ACCESS_DENIED;
+  }
   if ($page->locked) {
     if ($disable) {
       drupal_set_message(t('Unable to disable due to lock.'));
index af3db58..6206a7d 100644 (file)
@@ -24,6 +24,7 @@ function page_manager_schema_1() {
       'identifier' => 'handler',
       'bulk export' => TRUE,
       'export callback' => 'page_manager_export_task_handler',
+      'primary key' => 'did',
       'api' => array(
         'owner' => 'page_manager',
         'api' => 'pages_default',
index 4a0134b..22a560a 100644 (file)
@@ -44,11 +44,11 @@ function page_manager_permission() {
 
 /**
  * Implements hook_ctools_plugin_directory() to let the system know
- * we implement task and task_handler plugins.
+ * where our task and task_handler plugins are.
  */
-function page_manager_ctools_plugin_directory($module, $plugin) {
-  if ($module == 'page_manager') {
-    return 'plugins/' . $plugin;
+function page_manager_ctools_plugin_directory($owner, $plugin_type) {
+  if ($owner == 'page_manager') {
+    return 'plugins/' . $plugin_type;
   }
 }
 
@@ -56,6 +56,13 @@ function page_manager_ctools_plugin_directory($module, $plugin) {
  * Delegated implementation of hook_menu().
  */
 function page_manager_menu() {
+  // For some reason, some things can activate modules without satisfying
+  // dependencies. I don't know how, but this helps prevent things from
+  // whitescreening when this happens.
+  if (!module_exists('ctools')) {
+    return;
+  }
+
   $items = array();
   $base = array(
     'access arguments' => array('use page manager'),
@@ -128,6 +135,13 @@ function page_manager_menu() {
  * Get a list of all tasks and delegate to them.
  */
 function page_manager_menu_alter(&$items) {
+  // For some reason, some things can activate modules without satisfying
+  // dependencies. I don't know how, but this helps prevent things from
+  // whitescreening when this happens.
+  if (!module_exists('ctools')) {
+    return;
+  }
+
   $tasks = page_manager_get_tasks();
 
   foreach ($tasks as $task) {
@@ -149,6 +163,13 @@ function page_manager_menu_alter(&$items) {
  * Implements hook_theme()
  */
 function page_manager_theme() {
+  // For some reason, some things can activate modules without satisfying
+  // dependencies. I don't know how, but this helps prevent things from
+  // whitescreening when this happens.
+  if (!module_exists('ctools')) {
+    return;
+  }
+
   $base = array(
     'path' => drupal_get_path('module', 'page_manager') . '/theme',
     'file' => 'page_manager.theme.inc',
@@ -718,6 +739,8 @@ function page_manager_get_task_subtasks($task) {
   if ($function = ctools_plugin_get_function($task, 'subtasks callback')) {
     return $function($task);
   }
+
+  return array();
 }
 
 /**
@@ -1043,26 +1066,13 @@ function page_manager_page_manager_handlers_list() {
   $handlers = ctools_export_load_object('page_manager_handlers');
   foreach ($handlers as $handler) {
     if (in_array($handler->task, $types)) {
-      $list[$handler->name] = "$handler->task: {$handler->conf['title']} ($handler->name)";
+      $list[$handler->name] = check_plain("$handler->task: " . $handler->conf['title'] . " ($handler->name)");
     }
   }
   return $list;
 }
 
 /**
- * Callback to list pages available for export.
- */
-function page_manager_page_manager_pages_list() {
-  $list = array();
-
-  $pages = ctools_export_load_object('page_manager_pages', 'all');
-  foreach ($pages as $page) {
-    $list[$page->name] = "$page->admin_title ($page->name)";
-  }
-  return $list;
-}
-
-/**
  * Callback to bulk export page manager pages.
  */
 function page_manager_page_manager_pages_to_hook_code($names = array(), $name = 'foo') {
@@ -1112,3 +1122,47 @@ function page_manager_get_current_page($page = NULL) {
 
   return $current;
 }
+
+/**
+ * Implementation of hook_panels_dashboard_blocks().
+ *
+ * Adds page information to the Panels dashboard.
+ */
+function page_manager_panels_dashboard_blocks(&$vars) {
+  $vars['links']['page_manager'] = array(
+    'weight' => -100,
+    'title' => l(t('Panel page'), 'admin/build/pages/add'),
+    'description' => t('Panel pages can be used as landing pages. They have a URL path, accept arguments and can have menu entries.'),
+  );
+
+  module_load_include('inc', 'page_manager', 'page_manager.admin');
+  $tasks = page_manager_get_tasks_by_type('page');
+  $pages = array('operations' => array());
+
+  page_manager_get_pages($tasks, $pages);
+  $count = 0;
+  $rows = array();
+  foreach ($pages['rows'] as $id => $info) {
+    $rows[] = array(
+      'data' => array(
+        $info['data']['title'],
+        $info['data']['operations'],
+      ),
+      'class' => $info['class'],
+    );
+
+    // Only show 10.
+    if (++$count >= 10) {
+      break;
+    }
+  }
+
+  $vars['blocks']['page_manager'] = array(
+    'weight' => -100,
+    'title' => t('Manage pages'),
+    'link' => l(t('Go to list'), 'admin/build/pages'),
+    'content' => theme('table', array(), $rows, array('class' => 'panels-manage')),
+    'class' => 'dashboard-pages',
+    'section' => 'right',
+  );
+}
index 72269af..c13cb69 100644 (file)
@@ -37,6 +37,10 @@ function page_manager_page_menu(&$items, $task) {
     'page arguments' => array('page_manager_page_import_subtask', 'page'),
     'type' => MENU_LOCAL_TASK,
   ) + $base;
+  if ($access_callback == 'user_access') {
+    $items['admin/build/pages/import']['access callback'] = 'ctools_access_multiperm';
+    $items['admin/build/pages/import']['access arguments'][] = 'use PHP for block visibility';
+  }
 
   // AJAX callbacks for argument modal.
   $items['admin/build/pages/argument'] = array(
@@ -188,6 +192,7 @@ function page_manager_page_menu_item($task, $menu, $access_arguments, $page_argu
  * Page callback to add a subtask.
  */
 function page_manager_page_add_subtask($task_name = NULL, $step = NULL) {
+  ctools_include('context');
   $task = page_manager_get_task('page');
   $task_handler_plugins = page_manager_get_task_handler_plugins($task);
   if (empty($task_handler_plugins)) {
@@ -233,7 +238,17 @@ function page_manager_page_add_subtask($task_name = NULL, $step = NULL) {
     }
 
     $form_info['path'] = "admin/build/pages/add/$task_name/%step";
+  }
+  else {
+    $new_page = page_manager_page_new();
+    $new_page->name = NULL;
 
+    $page = new stdClass();
+    page_manager_page_new_page_cache($new_page, $page);
+    $form_info['path'] = 'admin/build/pages/add/%task_name/%step';
+  }
+
+  if ($step && $step != 'basic') {
     $handler_plugin = page_manager_get_task_handler($page->handler);
 
     $form_info['forms'] += $handler_plugin['forms'];
@@ -275,12 +290,6 @@ function page_manager_page_add_subtask($task_name = NULL, $step = NULL) {
     }
   }
   else {
-    $new_page = page_manager_page_new();
-    $new_page->name = NULL;
-
-    $page = new stdClass();
-    page_manager_page_new_page_cache($new_page, $page);
-    $form_info['path'] = 'admin/build/pages/add/%task_name/%step';
     $form_info['show trail'] = FALSE;
     $form_info['order'] = array(
       'basic' => t('Basic settings'),
@@ -499,7 +508,7 @@ function page_manager_page_form_basic_validate(&$form, &$form_state) {
 
   $found = FALSE;
   $error = FALSE;
-  foreach (explode('/', $form_state['values']['path']) as $bit) {
+  foreach (explode('/', $form_state['values']['path']) as $position => $bit) {
     if (!isset($bit) || $bit === '') {
       continue;
     }
@@ -512,6 +521,11 @@ function page_manager_page_form_basic_validate(&$form, &$form_state) {
       if ($found) {
         form_error($form['path'], t('You cannot have a dynamic path element after an optional path element.'));
       }
+
+      if ($position == 0) {
+        form_error($form['path'], t('The first element in a path may not be dynamic.'));
+      }
+
       $path[] = '%';
     }
     else if ($bit[0] == '!') {
@@ -580,6 +594,13 @@ function page_manager_page_form_basic_submit(&$form, &$form_state) {
     $cache->subtask_id = $page->name;
     $plugin = page_manager_get_task_handler($cache->handler);
 
+    // If they created and went back, there might be old, dead handlers
+    // that are not going to be added.
+    //
+    // Remove them:
+    $cache->handlers = array();
+    $cache->handler_info = array();
+
     // Create a new handler.
     $handler = page_manager_new_task_handler($plugin);
     $title = !empty($form_state['values']['title']) ? $form_state['values']['title'] : $plugin['title'];
@@ -1002,7 +1023,7 @@ function page_manager_page_subtask_argument_ajax($step = NULL, $task_name = NULL
 
   // With 'modal' and 'ajax' true, rendering automatically happens here so
   // we do nothing with the result.
-  ctools_wizard_multistep_form($form_info, $step, $form_state);
+  $output = ctools_wizard_multistep_form($form_info, $step, $form_state);
 }
 
 /**
@@ -1433,57 +1454,6 @@ function page_manager_page_form_clone_submit(&$form, &$form_state) {
 }
 
 /**
- * When adding or cloning a new page, this creates a new page cache
- * and adds our page to it.
- *
- * This does not check to see if the existing cache is already locked.
- * This must be done beforehand.
- *
- * @param &$page
- *   The page to create.
- * @param &$cache
- *   The cache to use. If the cache has any existing task handlers,
- *   they will be marked for deletion. This may be a blank object.
- */
-function page_manager_page_new_page_cache(&$page, &$cache) {
-  // Does a page already exist? If so, we are overwriting it so
-  // take its pid.
-  if (!empty($cache->subtask) && !empty($cache->subtask['subtask']) && !empty($cache->subtask['subtask']->pid)) {
-    $page->pid = $cache->subtask['subtask']->pid;
-  }
-  else {
-    $cache->new = TRUE;
-  }
-
-  $cache->task_name = page_manager_make_task_name('page', $page->name);
-  $cache->task_id = 'page';
-  $cache->task = page_manager_get_task('page');
-  $cache->subtask_id = $page->name;
-  $page->export_type = EXPORT_IN_DATABASE;
-  $page->type = t('Normal');
-  $cache->subtask = page_manager_page_build_subtask($cache->task, $page);
-
-  if (isset($cache->handlers)) {
-    foreach($cache->handlers as $id => $handler) {
-      $cache->handler_info[$id]['changed'] = PAGE_MANAGER_CHANGED_DELETED;
-    }
-  }
-  else {
-    $cache->handlers = array();
-    $cache->handler_info = array();
-  }
-
-  if (!empty($page->default_handlers)) {
-    foreach ($page->default_handlers as $id => $handler) {
-      page_manager_handler_add_to_page($cache, $handler);
-    }
-  }
-
-  $cache->locked = FALSE;
-  $cache->changed = TRUE;
-}
-
-/**
  * Entry point to export a page.
  */
 function page_manager_page_form_delete(&$form, &$form_state) {
index 6fa7eb9..624d060 100644 (file)
@@ -729,3 +729,54 @@ function page_manager_page_recalculate_arguments(&$page) {
   }
   $page->arguments = $args;
 }
+
+/**
+ * When adding or cloning a new page, this creates a new page cache
+ * and adds our page to it.
+ *
+ * This does not check to see if the existing cache is already locked.
+ * This must be done beforehand.
+ *
+ * @param &$page
+ *   The page to create.
+ * @param &$cache
+ *   The cache to use. If the cache has any existing task handlers,
+ *   they will be marked for deletion. This may be a blank object.
+ */
+function page_manager_page_new_page_cache(&$page, &$cache) {
+  // Does a page already exist? If so, we are overwriting it so
+  // take its pid.
+  if (!empty($cache->subtask) && !empty($cache->subtask['subtask']) && !empty($cache->subtask['subtask']->pid)) {
+    $page->pid = $cache->subtask['subtask']->pid;
+  }
+  else {
+    $cache->new = TRUE;
+  }
+
+  $cache->task_name = page_manager_make_task_name('page', $page->name);
+  $cache->task_id = 'page';
+  $cache->task = page_manager_get_task('page');
+  $cache->subtask_id = $page->name;
+  $page->export_type = EXPORT_IN_DATABASE;
+  $page->type = t('Normal');
+  $cache->subtask = page_manager_page_build_subtask($cache->task, $page);
+
+  if (isset($cache->handlers)) {
+    foreach($cache->handlers as $id => $handler) {
+      $cache->handler_info[$id]['changed'] = PAGE_MANAGER_CHANGED_DELETED;
+    }
+  }
+  else {
+    $cache->handlers = array();
+    $cache->handler_info = array();
+  }
+
+  if (!empty($page->default_handlers)) {
+    foreach ($page->default_handlers as $id => $handler) {
+      page_manager_handler_add_to_page($cache, $handler);
+    }
+  }
+
+  $cache->locked = FALSE;
+  $cache->changed = TRUE;
+}
index 3cc0243..49cd3a8 100644 (file)
@@ -45,24 +45,64 @@ function page_manager_search_page_manager_tasks() {
 /**
  * Callback defined by page_manager_search_page_manager_tasks().
  *
- * Alter the node view input so that node view comes to us rather than the
- * normal node view process.
+ * Alter the search tabs to work with page manager. The search flow is
+ * quite odd, and tracing through the code takes hours to realize
+ * that the tab you click on does not normally actually handle
+ * the search. This tries to account for that.
+ *
+ * Note to module authors: This tends to work a lot better with modules
+ * that override their own search pages if their _alter runs *before*
+ * this one.
  */
 function page_manager_search_menu_alter(&$items, $task) {
+  // We are creating two sets of tabs. One set is for searching without
+  // keywords. A second set is for searching *with* keywords. This
+  // is necessary because search/node/% and search/node need to be
+  // different due to the way the search menu items function.
+
+  // Go through each search module item.
   foreach (module_implements('search') as $name) {
-    if (variable_get('page_manager_search_disabled_' . $name, TRUE)) {
+    // Do not bother with search menu items that should not have search tabs.
+    if (!module_invoke($name, 'search', 'name')) {
+      continue;
+    }
+
+    // Put these items under the default search tab which is node.
+    $items["search/$name/%menu_tail"]['tab_parent'] = "search/node/%menu_tail";
+    $items["search/$name/%menu_tail"]['tab_root'] = "search/node/%menu_tail";
+
+    $callback = $items["search/$name/%menu_tail"]['page callback'];
+
+    // Even if a search page is not implemented, we need to add an extra
+    // entry anyway, for two reasons.
+    //
+    // 1) The 'search' menu entry actually handles all entries by default
+    // and that is going to be bad if the node search is overridden and
+    // 2) We need to have dual entries to make sure that the tabs are right.
+    if (variable_get('page_manager_search_disabled_' . $name, TRUE) || ($callback != 'search_view' && !variable_get('page_manager_override_anyway', FALSE))) {
       $items["search/$name"] = $items["search/$name/%menu_tail"];
-      // Since we may well be overriding the top level 'search' function
-      // create a 'search/%name' that goes directly where it is supposed
-      // to go.
+
+      // Put these items under the real search tab.
+      $items["search/$name"]['tab_parent'] = 'search';
+      $items["search/$name"]['tab_root'] = 'search';
+
       if ($name == 'node') {
         $items["search/$name"]['type'] = MENU_DEFAULT_LOCAL_TASK;
+        // The default tab should always be left weighted. Because of the way
+        // menu sorts, this item tends to float around if not weighted.
+        $items["search/$name"]['weight'] = -10;
+        $items["search/$name/%menu_tail"]['weight'] = -10;
       }
+
+      if ($callback == 'search_view' || variable_get('page_manager_override_anyway', FALSE)) {
+        $items["search/$name/%menu_tail"]['page callback'] = 'page_manager_search_view';
+        $items["search/$name/%menu_tail"]['file path'] = $task['path'];
+        $items["search/$name/%menu_tail"]['file'] = $task['file'];
+      }
+
       continue;
     }
 
-    // Override the node view handler for our purpose.
-    $callback = $items["search/$name/%menu_tail"]['page callback'];
     if ($callback == 'search_view' || variable_get('page_manager_override_anyway', FALSE)) {
       $items["search/$name/%menu_tail"]['page callback'] = 'page_manager_search_page';
       $items["search/$name/%menu_tail"]['file path'] = $task['path'];
@@ -74,10 +114,19 @@ function page_manager_search_menu_alter(&$items, $task) {
       $items["search/$name"] = $items["search/$name/%menu_tail"];
       $items["search/$name/%menu_tail"]['page arguments'] = array(1, 2);
 
+      // Put these items under the real search tab.
+      $items["search/$name"]['tab_parent'] = 'search';
+      $items["search/$name"]['tab_root'] = 'search';
+
       // Content search is the default search link, so we have to override
       // the default task as well.
       if ($name == 'node') {
         $items["search/$name"]['type'] = MENU_DEFAULT_LOCAL_TASK;
+        // The default tab should always be left weighted. Because of the way
+        // menu sorts, this item tends to float around if not weighted.
+        $items["search/$name"]['weight'] = -10;
+        $items["search/$name/%menu_tail"]['weight'] = -10;
+
         $items["search"]['page callback'] = 'page_manager_search_page';
         $items["search"]['page arguments'] = array('node');
         $items["search"]['file path'] = $task['path'];
@@ -95,6 +144,20 @@ function page_manager_search_menu_alter(&$items, $task) {
 }
 
 /**
+ * Replacement function for normal search view.
+ *
+ * This function resets the active trail because menu system loses track
+ * of it due to the special way we're handling search items.
+ */
+function page_manager_search_view($type = 'node') {
+  ctools_include('menu');
+  menu_set_active_trail(ctools_get_menu_trail('search/' . $type));
+
+  module_load_include('inc', 'search', 'search.pages');
+  return search_view($type);
+}
+
+/**
  * Entry point for our overridden node view.
  *
  * This function asks its assigned handlers who, if anyone, would like
@@ -102,6 +165,9 @@ function page_manager_search_menu_alter(&$items, $task) {
  * node view, which is node_page_view().
  */
 function page_manager_search_page($type) {
+  ctools_include('menu');
+  menu_set_active_trail(ctools_get_menu_trail('search/' . $type));
+
   // Get the arguments and construct a keys string out of them.
   $args = func_get_args();
 
@@ -187,7 +253,9 @@ function page_manager_search_enable($cache, $status) {
  */
 function page_manager_search_subtasks($task) {
   foreach (module_implements('search') as $name) {
-    $return[$name] = page_manager_search_build_subtask($task, $name);
+    if(module_invoke($name, 'search', 'name')) {
+      $return[$name] = page_manager_search_build_subtask($task, $name);
+    }
   }
 
   return $return;
index a8b75f3..1548c6c 100644 (file)
@@ -35,7 +35,7 @@ function page_manager_term_view_page_manager_tasks() {
       // Even though we don't have subtasks, this allows us to save our settings.
       'save subtask callback' => 'page_manager_term_view_save',
 
-      // Callback to add items to the page managertask administration form:
+      // Callback to add items to the page manager task administration form:
       'task admin' => 'page_manager_term_view_task_admin',
 
       // This is task uses 'context' handlers and must implement these to give the
@@ -44,32 +44,16 @@ function page_manager_term_view_page_manager_tasks() {
       'get arguments' => 'page_manager_term_view_get_arguments',
       'get context placeholders' => 'page_manager_term_view_get_contexts',
 
-      // Allow additional operations
-      'operations' => array(
-        'settings' => array(
-          'title' => t('Settings'),
-          'description' => t('Update settings specific to the taxonomy term view.'),
-        ),
-        // This lets it automatically add relevant information for task handlers.
-        'handlers' => array('type' => 'handlers'),
-      ),
-
       // Allow this to be enabled or disabled:
       'disabled' => variable_get('page_manager_term_view_disabled', TRUE),
       'enable callback' => 'page_manager_term_view_enable',
 
+      // Allow additional operations
       'operations' => array(
         'settings' => array(
-          'type' => 'group',
-          'class' => 'operations-settings',
           'title' => t('Settings'),
-          'children' => array(
-            'basic' => array(
-              'title' => t('Basic'),
-              'description' => t('Edit name, path and other basic settings for the page.'),
-              'form' => 'page_manager_term_view_settings',
-            ),
-          ),
+          'description' => t('Edit name, path and other basic settings for the page.'),
+          'form' => 'page_manager_term_view_settings',
         ),
       ),
     );
index 8722364..c706488 100644 (file)
@@ -85,7 +85,7 @@ function page_manager_user_view($account) {
   }
   else {
     //fire off "view" op so that triggers still work
-    module_invoke_all('user', 'view', array(), $account);
+    user_module_invoke('view', $array = array(), $account);
   }
   return $output;
 }
index 1e45149..cd78555 100644 (file)
@@ -35,7 +35,7 @@ function ctools_context_exists_ctools_access_check($conf, $context) {
   // xor returns false if the two bools are the same, and true if they are not.
   // i.e, if we asked for context_exists and it does, return true.
   // If we asked for context does not exist and it does, return false.
-  return (empty($context->data) xor !empty($conf['context_exists']));
+  return (empty($context->data) xor !empty($conf['exists']));
 }
 
 /**
index ae8faac..2c2ba21 100644 (file)
@@ -18,7 +18,7 @@ if (module_exists('locale')) {
     'default' => array('language' => array()),
     'settings form' => 'ctools_node_language_ctools_access_settings',
     'settings form submit' => 'ctools_node_language_ctools_access_settings_submit',
-    'summary' => 'ctools_node_language_ctools_acesss_summary',
+    'summary' => 'ctools_node_language_ctools_access_summary',
     'required context' => new ctools_context_required(t('Node'), 'node'),
   );
 }
@@ -88,7 +88,7 @@ function ctools_node_language_ctools_access_check($conf, $context) {
 /**
  * Provide a summary description based upon the checked node_languages.
  */
-function ctools_node_language_ctools_acesss_summary($conf, $context) {
+function ctools_node_language_ctools_access_summary($conf, $context) {
   $languages = array(
     'current' => t('Current site language'),
     'default' => t('Default site language'),
index 35e0eee..bcb75a4 100644 (file)
@@ -16,7 +16,7 @@ $plugin = array(
   'callback' => 'ctools_perm_ctools_access_check',
   'default' => array('perm' => 'access content'),
   'settings form' => 'ctools_perm_ctools_access_settings',
-  'summary' => 'ctools_perm_ctools_acesss_summary',
+  'summary' => 'ctools_perm_ctools_access_summary',
   'required context' => new ctools_context_required(t('User'), 'user'),
 );
 
@@ -59,7 +59,7 @@ function ctools_perm_ctools_access_check($conf, $context) {
 /**
  * Provide a summary description based upon the checked roles.
  */
-function ctools_perm_ctools_acesss_summary($conf, $context) {
+function ctools_perm_ctools_access_summary($conf, $context) {
   if (!isset($conf['perm'])) {
     return t('Error, unset permission');
   }
index 43ba6bc..d100dee 100644 (file)
@@ -16,7 +16,7 @@ $plugin = array(
   'callback' => 'ctools_php_ctools_access_check',
   'default' => array('description' => '', 'php' => ''),
   'settings form' => 'ctools_php_ctools_access_settings',
-  'summary' => 'ctools_php_ctools_acesss_summary',
+  'summary' => 'ctools_php_ctools_access_summary',
   'all contexts' => TRUE,
 );
 
@@ -59,6 +59,6 @@ function ctools_php_ctools_access_check($__conf, $contexts) {
 /**
  * Provide a summary description based upon the checked roles.
  */
-function ctools_php_ctools_acesss_summary($conf, $contexts) {
-  return check_plain($conf['description']);
+function ctools_php_ctools_access_summary($conf, $contexts) {
+  return !empty($conf['description']) ? check_plain($conf['description']) : t('No description');
 }
index f6ff199..24f4766 100644 (file)
@@ -17,7 +17,7 @@ $plugin = array(
   'default' => array('rids' => array()),
   'settings form' => 'ctools_role_ctools_access_settings',
   'settings form submit' => 'ctools_role_ctools_access_settings_submit',
-  'summary' => 'ctools_role_ctools_acesss_summary',
+  'summary' => 'ctools_role_ctools_access_summary',
   'required context' => new ctools_context_required(t('User'), 'user'),
 );
 
@@ -59,7 +59,7 @@ function ctools_role_ctools_access_check($conf, $context) {
 /**
  * Provide a summary description based upon the checked roles.
  */
-function ctools_role_ctools_acesss_summary($conf, $context) {
+function ctools_role_ctools_access_summary($conf, $context) {
   if (!isset($conf['rids'])) {
     $conf['rids'] = array();
   }
index c641e44..81cc64b 100644 (file)
@@ -18,7 +18,7 @@ if (module_exists('locale')) {
     'default' => array('language' => array()),
     'settings form' => 'ctools_site_language_ctools_access_settings',
     'settings form submit' => 'ctools_site_language_ctools_access_settings_submit',
-    'summary' => 'ctools_site_language_ctools_acesss_summary',
+    'summary' => 'ctools_site_language_ctools_access_summary',
   );
 }
 
@@ -63,7 +63,7 @@ function ctools_site_language_ctools_access_check($conf, $context) {
 /**
  * Provide a summary description based upon the checked site_languages.
  */
-function ctools_site_language_ctools_acesss_summary($conf, $context) {
+function ctools_site_language_ctools_access_summary($conf, $context) {
   $languages = array(
     'default' => t('Default site language'),
   );
index 772b70b..3c91836 100644 (file)
  */
 $plugin = array(
   'title' => t("Taxonomy: vocabulary"),
-  'description' => t('Control access by term vocabulary.'),
+  'description' => t('Control access by vocabulary.'),
   'callback' => 'ctools_term_vocabulary_ctools_access_check',
   'default' => array('vids' => array()),
   'settings form' => 'ctools_term_vocabulary_ctools_access_settings',
   'settings form submit' => 'ctools_term_vocabulary_ctools_access_settings_submit',
-  'summary' => 'ctools_term_vocabulary_ctools_acesss_summary',
-  'required context' => new ctools_context_required(t('Term'), array('term', 'terms')),
+  'summary' => 'ctools_term_vocabulary_ctools_access_summary',
+  'required context' => new ctools_context_required(t('Vocabulary'), array('term', 'terms', 'vocabulary')),
 );
 
 /**
@@ -35,7 +35,7 @@ function ctools_term_vocabulary_ctools_access_settings(&$form, &$form_state, $co
     '#type' => 'checkboxes',
     '#title' => t('Vocabularies'),
     '#options' => $options,
-    '#description' => t('Only terms in the checked vocabularies will be valid.'),
+    '#description' => t('Only the checked vocabularies will be valid.'),
     '#default_value' => $conf['vids'],
   );
 }
@@ -67,7 +67,7 @@ function ctools_term_vocabulary_ctools_access_check($conf, $context) {
 /**
  * Provide a summary description based upon the checked term_vocabularys.
  */
-function ctools_term_vocabulary_ctools_acesss_summary($conf, $context) {
+function ctools_term_vocabulary_ctools_access_summary($conf, $context) {
   if (!isset($conf['type'])) {
     $conf['type'] = array();
   }
index 9e51749..381ef5a 100644 (file)
@@ -43,10 +43,12 @@ function ctools_term_context($arg = NULL, $conf = NULL, $empty = FALSE) {
 
     case 'term':
       $terms = taxonomy_get_term_by_name($arg);
-      if (count($terms) != 1) {
+
+      $conf['vids'] = is_array($conf['vids']) ? array_filter($conf['vids']) : NULL;
+      if ((count($terms) > 1) && isset($conf['vids'])) {
         foreach ($terms as $potential) {
           foreach ($conf['vids'] as $vid => $active) {
-            if ($active == 1 && $potential->vid == $vid) {
+            if ($active && $potential->vid == $vid) {
               $term = $potential;
               // break out of the foreaches AND the case
               break 3;
@@ -85,6 +87,19 @@ function ctools_term_settings_form(&$form, &$form_state, $conf) {
     '#suffix' => '</div>',
   );
 
+  $vocabularies = taxonomy_get_vocabularies();
+  $options = array();
+  foreach ($vocabularies as $vid => $vocab) {
+    $options[$vid] = $vocab->name;
+  }
+  $form['settings']['vids'] = array(
+    '#title' => t('Limit to these vocabularies'),
+    '#type' => 'checkboxes',
+    '#options' => $options,
+    '#default_value' => !empty($conf['vids']) ? $conf['vids'] : array(),
+    '#description' => t('If no vocabularies are checked, terms from all vocabularies will be accepted.'),
+  );
+
   $form['settings']['breadcrumb'] = array(
     '#title' => t('Inject hierarchy into breadcrumb trail'),
     '#type' => 'checkbox',
index b39d02d..e27f509 100644 (file)
@@ -19,9 +19,21 @@ $plugin = array(
   // And this is just the administrative title.
   // All our callbacks are named according to the standard pattern and can be deduced.
   'title' => t('Block'),
+  'content type' => 'ctools_block_content_type_content_type',
 );
 
 /**
+ * Return the block content types with the specified $subtype_id.
+ */
+function ctools_block_content_type_content_type($subtype_id) {
+  list($module, $delta) = explode('-', $subtype_id, 2);
+  $module_blocks = module_invoke($module, 'block', 'list');
+  if (isset($module_blocks[$delta])) {
+    return _ctools_block_content_type_content_type($module, $delta, $module_blocks[$delta]);
+  }
+}
+
+/**
  * Return all block content types available.
  *
  * Modules wanting to make special adjustments the way that CTools handles their blocks
@@ -34,19 +46,7 @@ function ctools_block_content_type_content_types() {
     $module_blocks = module_invoke($module, 'block', 'list');
     if ($module_blocks) {
       foreach ($module_blocks as $delta => $block) {
-        // strip_tags used because it goes through check_plain and that
-        // just looks bad.
-        $info = array(
-          'title' => strip_tags($block['info']),
-        );
-
-        // Ask around for further information by invoking the hook_block() extension.
-        $function = $module . '_ctools_block_info';
-        if (!function_exists($function)) {
-          $function = 'ctools_default_block_info';
-        }
-        $function($module, $delta, $info);
-
+        $info = _ctools_block_content_type_content_type($module, $delta, $block);
         // this check means modules can remove their blocks; particularly useful
         // if they offer the block some other way (like we do for views)
         if ($info) {
@@ -59,6 +59,26 @@ function ctools_block_content_type_content_types() {
 }
 
 /**
+ * Return an info array for a specific block.
+ */
+function _ctools_block_content_type_content_type($module, $delta, $block) {
+  // strip_tags used because it goes through check_plain and that
+  // just looks bad.
+  $info = array(
+    'title' => strip_tags($block['info']),
+  );
+
+  // Ask around for further information by invoking the hook_block() extension.
+  $function = $module . '_ctools_block_info';
+  if (!function_exists($function)) {
+    $function = 'ctools_default_block_info';
+  }
+  $function($module, $delta, $info);
+
+  return $info;
+}
+
+/**
  * Output function for the 'block' content type. Outputs a block
  * based on the module and delta supplied in the configuration.
  */
@@ -73,8 +93,13 @@ function ctools_block_content_type_render($subtype, $conf) {
   $block->module = $module;
   $block->delta = $delta;
 
-  if ($module == 'block' && empty($conf['override_title'])) {
-    $block->subject = db_query('SELECT title FROM {blocks} WHERE module = :module AND delta = :delta', array(':module' => 'block', ':delta' => $delta))->fetchField();
+  // $block->title is not set for the blocks returned by block_block() (the
+  // Block module adds the title in block_list() instead), so we look it up
+  // manually, unless the title is overridden and does not use the %title
+  // placeholder.
+  if ($module == 'block' && (empty($conf['override_title']) || strpos($conf['override_title_text'], '%title') !== FALSE)) {
+    // {block}.title is plain text, but this hook should return an HTML title.
+    $block->subject = check_plain(db_result(db_query("SELECT title FROM {blocks} WHERE module = '%s' AND delta = '%s'", 'block', $delta)));
   }
 
   if (isset($block->subject)) {
@@ -108,7 +133,8 @@ function ctools_block_content_type_render($subtype, $conf) {
   $block_visibility = db_query('SELECT title, pages, visibility FROM {blocks} WHERE module = :module AND delta = :delta', array(':module' => $block->module, ':delta' => $block->delta))->fetchObject();
 
   if ($block->module == 'block') {
-    $block->title = $block_visibility->title;
+    // {block}.title is plain text, but this hook should return an HTML title.
+    $block->title = check_plain($block_visibility->title);
   }
 
   if (empty($conf['block_visibility'])) {
@@ -226,7 +252,9 @@ function ctools_block_content_type_admin_title($subtype, $conf) {
     return t('Deleted/missing block @module-@delta', array('@module' => $module, '@delta' => $delta));
   }
 
-  $title = filter_xss_admin($block[$delta]['info']);
+  // The block description reported by hook_block() is plain text, but the title
+  // reported by this hook should be HTML.
+  $title = check_plain($block[$delta]['info']);
   return $title;
 }
 
@@ -239,7 +267,10 @@ function ctools_block_content_type_admin_info($subtype, $conf) {
   $block = (object) module_invoke($module, 'block', 'view', $delta);
 
   // Sanitize the block because <script> tags can hose javascript up:
-  $block->content = filter_xss_admin($block->content);
+  if (!empty($block->content)) {
+    $block->content = filter_xss_admin($block->content);
+  }
+
   if (!empty($block) && !empty($block->subject)) {
     $block->title = $block->subject;
     return $block;
index 5acc023..5274b8d 100644 (file)
  * by the system that includes this file.
  */
 $plugin = array(
-  'single' => TRUE,
   'no title override' => TRUE,
-  'title' => t('New custom content'),
-  'icon' => 'icon_block_custom.png',
-  'description' => t('Create a completely custom piece of HTML content.'),
-  // Make this a top level category so it appears higher in UIs that support
-  // that.
-  'top level' => TRUE,
-  'category' => t('Custom'),
   'defaults' => array('admin_title' => '', 'title' => '', 'body' => '', 'format' => FILTER_FORMAT_DEFAULT, 'substitute' => TRUE),
-  // render callback is automatically deduced:
-  // 'render callback' => 'ctools_custom_content_type_render',
   'js' => array('misc/autocomplete.js', 'misc/textarea.js', 'misc/collapse.js'),
+  // Make sure the edit form is only used for some subtypes.
+  'edit form' => '',
+  'add form' => '',
+  'edit text' => t('Edit'),
   'all contexts' => TRUE,
 );
 
 /**
+ * Return the custom content types with the specified $subtype_id.
+ */
+function ctools_custom_content_type_content_type($subtype_id) {
+  if ($subtype_id == 'custom') {
+    return _ctools_default_content_type_content_type();
+  }
+  else {
+    ctools_include('export');
+    $content = ctools_export_crud_load('ctools_custom_content', $subtype_id);
+    if ($content) {
+      return _ctools_custom_content_type_content_type($content);
+    }
+  }
+}
+
+/**
+ * Return all custom content types available.
+ */
+function ctools_custom_content_type_content_types() {
+  ctools_include('export');
+  $types = array();
+  $types['custom'] = _ctools_default_content_type_content_type();
+
+  foreach (ctools_export_crud_load_all('ctools_custom_content') as $name => $content) {
+    $types[$name] = _ctools_custom_content_type_content_type($content);
+  }
+  return $types;
+}
+
+/**
+ * Settings for the default custom content type.
+ *
+ * The default is the one that allows the user to actually create a type.
+ */
+function _ctools_default_content_type_content_type() {
+  $info = array(
+    'name' => 'custom',
+    'title' => t('New custom content'),
+    'top level' => TRUE,
+    'category' => t('Custom'),
+    'description' => t('Create a completely custom piece of HTML content.'),
+    'edit form' => 'ctools_custom_content_type_edit_form',
+    'all contexts' => TRUE,
+    'check editable' => 'ctools_custom_content_type_editable',
+  );
+
+  return $info;
+}
+
+/**
+ * Return an info array for a specific custom content type.
+ */
+function _ctools_custom_content_type_content_type($content) {
+  $info = array(
+    'name' => $content->name,
+    'title' => check_plain($content->admin_title),
+    'description' => check_plain($content->admin_description),
+    'category' => $content->category ? check_plain($content->category) : t('Miscellaneous'),
+    'all contexts' => TRUE,
+    'icon' => 'icon_block_custom.png',
+    // Store this here to make it easy to access.
+    'content' => $content,
+  );
+
+  return $info;
+}
+
+/**
+ * Given a subtype and a $conf, return the actual settings to use.
+ *
+ * The actual settings may be stored directly in the pane or this may
+ * be a pointer to re-usable content that may be in the database or in
+ * an export. We have to determine from the subtype whether or not it
+ * is local or shared custom content.
+ */
+function ctools_custom_content_type_get_conf($subtype, $conf) {
+  if ($subtype['name'] != 'custom') {
+    $settings = $subtype['content']->settings;
+    $settings['custom_type'] = 'fixed';
+    $settings['content'] = $subtype['content'];
+  }
+  else {
+    // This means they created it as custom content and then set it as
+    // reusable. Since we're not allowed to change the subtype, we're
+    // still stored as though we are local, but are pointing off to
+    // non-local.
+    if (!empty($conf['name'])) {
+      ctools_include('export');
+      $content = ctools_export_crud_load('ctools_custom_content', $conf['name']);
+      if ($content) {
+        $settings = $content->settings;
+        $settings['custom_type'] = 'fixed';
+        $settings['content'] = $content;
+        $settings['admin_title'] = $content->admin_title;
+      }
+      else {
+        $content = ctools_export_crud_new('ctools_custom_content');
+        $content->name = $conf['name'];
+        $settings = array(
+          'admin_title' => t('Missing/deleted content'),
+          'title' => '',
+          'body' => '',
+          'format' => FILTER_FORMAT_DEFAULT,
+          'substitute' => TRUE,
+          'custom_type' => 'fixed',
+          'content' => $content,
+        );
+      }
+    }
+    // This means that it is created as custom and has not been set to
+    // reusable.
+    else {
+      $settings = $conf;
+      $settings['custom_type'] = 'local';
+    }
+  }
+
+  return $settings;
+}
+
+function ctools_custom_content_type_editable($content_type, $subtype, $conf) {
+  if ($subtype['name'] == 'custom' && !empty($conf['name'])) {
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+/**
  * Output function for the 'custom' content type. Outputs a custom
  * based on the module and delta supplied in the configuration.
  */
 function ctools_custom_content_type_render($subtype, $conf, $args, $contexts) {
+  $settings = ctools_custom_content_type_get_conf(ctools_custom_content_type_content_type($subtype), $conf);
+
   static $delta = 0;
 
   $block          = new stdClass();
   $block->subtype = ++$delta;
-  $block->title   = filter_xss_admin($conf['title']);
-  $block->content = check_markup($conf['body'], $conf['format'], FALSE);
+  $block->title   = filter_xss_admin($settings['title']);
 
   // Add keyword substitutions if we were configured to do so.
-  if (!empty($contexts) && !empty($conf['substitute'])) {
-    $block->content = ctools_context_keyword_substitute($block->content, array(), $contexts);
+  $content = $settings['body'];
+  if (!empty($contexts) && !empty($settings['substitute'])) {
+    $content = ctools_context_keyword_substitute($content, array(), $contexts);
   }
+
+  $block->content = check_markup($content, $settings['format'], FALSE);
   return $block;
 }
 
@@ -54,11 +181,19 @@ function ctools_custom_content_type_render($subtype, $conf, $args, $contexts) {
  * Callback to provide the administrative title of the custom content.
  */
 function ctools_custom_content_type_admin_title($subtype, $conf) {
+  $settings = ctools_custom_content_type_get_conf(ctools_custom_content_type_content_type($subtype), $conf);
+
   $output = t('Custom');
-  $title = !empty($conf['admin_title']) ? $conf['admin_title'] : $conf['title'];
+  $title = !empty($settings['admin_title']) ? $settings['admin_title'] : $settings['title'];
   if ($title) {
-    $output = t('Custom: @title', array('@title' => $title));
+    if ($settings['custom_type'] != 'fixed') {
+      $output = t('Custom: @title', array('@title' => $title));
+    }
+    else {
+      $output = $title;
+    }
   }
+
   return $output;
 }
 
@@ -67,14 +202,16 @@ function ctools_custom_content_type_admin_title($subtype, $conf) {
  * content as long as it's not PHP, which is too risky to render here.
  */
 function ctools_custom_content_type_admin_info($subtype, $conf) {
+  $settings = ctools_custom_content_type_get_conf(ctools_custom_content_type_content_type($subtype), $conf);
+
   $block = new stdClass();
-  $block->title = filter_xss_admin($conf['title']);
+  $block->title = filter_xss_admin($settings['title']);
   // We don't want to render php output on preview here, because if something is
   // wrong the whole display will be borked. So we check to see if the php
   // evaluator filter is being used, and make a temporary change to the filter
   // so that we get the printed php, not the eval'ed php.
   $php_filter = FALSE;
-  foreach (filter_list_format($conf['format']) as $filter) {
+  foreach (filter_list_format($settings['format']) as $filter) {
     if ($filter->module == 'php') {
       $php_filter = TRUE;
       break;
@@ -83,13 +220,15 @@ function ctools_custom_content_type_admin_info($subtype, $conf) {
   // If a php filter is active, just print the source, but only if the current
   // user has access to the actual filter.
   if ($php_filter) {
-    if (!filter_access($conf['format'])) {
+    if (!filter_access($settings['format'])) {
       return NULL;
     }
-    $block->content = '<pre>'. check_plain($conf['body']) .'</pre>';
+    $block->content = '<pre>'. check_plain($settings['body']) .'</pre>';
   }
   else {
-    $block->content = check_markup($conf['body'], $conf['format']);
+    // We also need to filter through XSS admin because <script> tags can
+    // cause javascript which will interfere with our ajax.
+    $block->content = filter_xss_admin(check_markup($settings['body'], $settings['format']));
   }
   return $block;
 }
@@ -98,34 +237,40 @@ function ctools_custom_content_type_admin_info($subtype, $conf) {
  * Returns an edit form for the custom type.
  */
 function ctools_custom_content_type_edit_form(&$form, &$form_state) {
-  $conf = $form_state['conf'];
+  $settings = ctools_custom_content_type_get_conf($form_state['subtype'], $form_state['conf']);
+  $form_state['settings'] = $settings;
+
+  if ($settings['custom_type'] == 'fixed') {
+    return; // no form for this case.
+  }
+
   $form['admin_title'] = array(
     '#type' => 'textfield',
-    '#default_value' => isset($conf['admin_title']) ? $conf['admin_title'] : '',
+    '#default_value' => isset($settings['admin_title']) ? $settings['admin_title'] : '',
     '#title' => t('Administrative title'),
     '#description' => t('This title will be used administratively to identify this pane. If blank, the regular title will be used.'),
   );
 
   $form['title'] = array(
     '#type' => 'textfield',
-    '#default_value' => $conf['title'],
+    '#default_value' => $settings['title'],
     '#title' => t('Title'),
   );
 
   $form['body_field']['body'] = array(
     '#title' => t('Body'),
     '#type' => 'textarea',
-    '#default_value' => $conf['body'],
+    '#default_value' => $settings['body'],
   );
   $parents[] = 'format';
-  $form['body_field']['format'] = filter_form($conf['format'], 1, $parents);
+  $form['body_field']['format'] = filter_form($settings['format'], 1, $parents);
 
   if (!empty($form_state['contexts'])) {
     $form['substitute'] = array(
       '#type' => 'checkbox',
       '#title' => t('Use context keywords'),
       '#description' => t('If checked, context keywords will be substituted in this content.'),
-      '#default_value' => !empty($conf['substitute']),
+      '#default_value' => !empty($settings['substitute']),
     );
     $form['contexts'] = array(
       '#title' => t('Substitutions'),
@@ -147,16 +292,102 @@ function ctools_custom_content_type_edit_form(&$form, &$form_state) {
     $form['contexts']['context'] = array('#value' => theme('table', $header, $rows));
   }
 
-  return $form;
+  if (!user_access('administer custom content') || !module_exists('ctools_custom_content')) {
+    return;
+  }
+
+  // Make the other form items dependent upon it.
+  ctools_include('dependent');
+  ctools_add_js('dependent');
+
+  $form['reusable'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Make this content reusable'),
+    '#default_value' => FALSE,
+  );
+
+  $form['name'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Machine name'),
+    '#description' => t('The machine readable name of this content. It must be unique, and it must contain only alphanumeric characters and underscores. Once created, you will not be able to change this value!'),
+    '#process' => array('ctools_dependent_process'),
+    '#dependency' => array('edit-reusable' => array(1)),
+  );
+
+  $form['category'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Category'),
+    '#description' => t('What category this content should appear in. If left blank the category will be "Miscellaneous".'),
+    '#process' => array('ctools_dependent_process'),
+    '#dependency' => array('edit-reusable' => array(1)),
+  );
+
+  $form['admin_description'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Administrative description'),
+    '#description' => t('A description of what this content is, does or is for, for administrative use.'),
+    '#process' => array('ctools_dependent_process'),
+    '#dependency' => array('edit-reusable' => array(1)),
+  );
+}
+
+function _ctools_custom_content_type_edit_save(&$content, $form_state) {
+  // Apply updates to the content object.
+  $content->category = $form_state['values']['category'];
+  $content->admin_title = $form_state['values']['admin_title'];
+  $content->admin_description = $form_state['values']['admin_description'];
+  foreach (array_keys($form_state['plugin']['defaults']) as $key) {
+    if (isset($form_state['values'][$key])) {
+      $content->settings[$key] = $form_state['values'][$key];
+    }
+  }
+
+  ctools_export_crud_save('ctools_custom_content', $content);
+}
+
+/**
+ * The validate form to ensure the custom content data is okay.
+ */
+function ctools_custom_content_type_edit_form_validate(&$form, &$form_state) {
+  if ($form_state['settings']['custom_type'] != 'fixed' && !empty($form_state['values']['reusable'])) {
+    if (empty($form_state['values']['name'])) {
+      form_error($form['name'], t('Name is required.'));
+    }
+
+    // Check for string identifier sanity
+    if (!preg_match('!^[a-z0-9_]+$!', $form_state['values']['name'])) {
+      form_error($form['name'], t('The name can only consist of lowercase letters, underscores, and numbers.'));
+      return;
+    }
+
+    // Check for name collision
+    if ($form_state['values']['name'] == 'custom' || (ctools_export_crud_load('ctools_custom_content', $form_state['values']['name']))) {
+      form_error($form['name'], t('Content with this name already exists. Please choose another name or delete the existing item before creating a new one.'));
+    }
+  }
 }
 
 /**
  * The submit form stores the data in $conf.
  */
 function ctools_custom_content_type_edit_form_submit(&$form, &$form_state) {
-  foreach (array_keys($form_state['plugin']['defaults']) as $key) {
-    if (isset($form_state['values'][$key])) {
-      $form_state['conf'][$key] = $form_state['values'][$key];
+  if ($form_state['settings']['custom_type'] == 'fixed') {
+    _ctools_custom_content_type_edit_save($form_state['settings']['content'], $form_state);
+  }
+  // If the 'reusable' checkbox was checked, we will create a new
+  // custom content and give it the proper values.
+  else if (!empty($form_state['values']['reusable'])) {
+    $content = ctools_export_crud_new('ctools_custom_content');
+    $content->name = $form_state['values']['name'];
+    _ctools_custom_content_type_edit_save($content, $form_state);
+    $form_state['conf']['name'] = $content->name;
+  }
+  else {
+    // Otherwise, just save values into $conf normally.
+    foreach (array_keys($form_state['plugin']['defaults']) as $key) {
+      if (isset($form_state['values'][$key])) {
+        $form_state['conf'][$key] = $form_state['values'][$key];
+      }
     }
   }
 }
index 1321c94..4f0d569 100644 (file)
@@ -8,27 +8,26 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_node_ctools_content_types() {
-  return array(
-    'title' => t('Node'),
-    'single' => TRUE,
-    'defaults' => array(
-      'nid' => '',
-      'teaser' => TRUE,
-      'links' => TRUE,
-      'leave_node_title' => FALSE,
-      'identifier' => '',
-    ),
-    'title' => t('Existing node'),
-    'icon' => 'icon_node.png',
-    'description' => t('Add a node from your site as content.'),
-    'category' => t('Custom'),
-    'top level' => TRUE,
-    'js' => array('misc/autocomplete.js'),
-  );
-}
+$plugin = array(
+  'title' => t('Node'),
+  'single' => TRUE,
+  'defaults' => array(
+    'nid' => '',
+    'links' => TRUE,
+    'leave_node_title' => FALSE,
+    'identifier' => '',
+    'build_mode' => 'teaser',
+  ),
+  'title' => t('Existing node'),
+  'icon' => 'icon_node.png',
+  'description' => t('Add a node from your site as content.'),
+  'category' => t('Custom'),
+  'top level' => TRUE,
+  'js' => array('misc/autocomplete.js'),
+);
 
 /**
  * Output function for the 'node' content type.
@@ -44,7 +43,9 @@ function ctools_node_content_type_render($subtype, $conf, $panel_args) {
   }
 
   foreach ($panel_args as $id => $arg) {
-    $nid = str_replace("@$id", $arg, $nid);
+    if (is_string($arg)) {
+      $nid = str_replace("@$id", $arg, $nid);
+    }
   }
 
   // Support node translation
@@ -77,17 +78,28 @@ function ctools_node_content_type_render($subtype, $conf, $panel_args) {
   $block->module = 'node';
   $block->delta = $node->nid;
 
-  $block->title = $node->title;
+  if (!empty($conf['link_node_title'])) {
+    $block->title = l($node->title, 'node/' . $node->nid);
+  }
+  else {
+    $block->title = check_plain($node->title);
+  }
 
   if (empty($conf['leave_node_title'])) {
     $node->title = NULL;
   }
+
   if (!empty($conf['identifier'])) {
     $node->panel_identifier = $conf['identifier'];
   }
 
+  // Handle existing configurations with the deprecated 'teaser' option.
+  if (isset($conf['teaser'])) {
+    $conf['build_mode'] = $conf['teaser'] ? 'teaser' : 'full';
+  }
+
   if (!isset($node->build_mode)) {
-    $node->build_mode = NODE_BUILD_NORMAL;
+    $node->build_mode = ($conf['build_mode'] == 'teaser' || $conf['build_mode'] == 'full') ? NODE_BUILD_NORMAL : $conf['build_mode'];
   }
 
   // FIXME some of the old behavior will likely not work after the straight
@@ -109,6 +121,14 @@ function ctools_node_content_type_edit_form(&$form, &$form_state) {
     '#description' => t('Advanced: if checked, do not touch the node title; this can cause the node title to appear twice unless your theme is aware of this.'),
   );
 
+  $form['link_node_title'] = array(
+    '#type' => 'checkbox',
+    '#default_value' => !empty($conf['link_node_title']),
+    '#title' => t('Link the node title to the node'),
+    '#description' => t('Check this box if you would like your pane title to link to the node.'),
+  );
+
+
   if ($form_state['op'] == 'add') {
     $form['nid'] = array(
       '#prefix' => '<div class="no-float">',
@@ -128,12 +148,6 @@ function ctools_node_content_type_edit_form(&$form, &$form_state) {
     );
   }
 
-  $form['teaser'] = array(
-    '#title' => t('Show only node teaser'),
-    '#type' => 'checkbox',
-    '#default_value' => $conf['teaser'],
-  );
-
   $form['links'] = array(
     '#type' => 'checkbox',
     '#default_value' => $conf['links'],
@@ -147,6 +161,38 @@ function ctools_node_content_type_edit_form(&$form, &$form_state) {
     '#description' => t('This identifier will be added as a template suggestion to display this node: node-panel-IDENTIFIER.tpl.php. Please see the Drupal theming guide for information about template suggestions.'),
   );
 
+  // CCK holds the registry of available build modes, but can hardly
+  // push them as options for the build mode options, so we break the normal
+  // rule of not directly relying on non-core modules.
+  if ($modes = module_invoke('content', 'build_modes')) {
+    $build_mode_options = array();
+    foreach ($modes as $key => $value) {
+      if (isset($value['views style']) && $value['views style']) {
+        $build_mode_options[$key] = $value['title'];
+      }
+    }
+  }
+  else {
+    $build_mode_options = array(
+      'teaser' => t('Teaser'),
+      'full' => t('Full node')
+    );
+  }
+
+  // Handle existing configurations with the deprecated 'teaser' option.
+  // Also remove the teaser key from the form_state.
+  if (isset($conf['teaser']) || !isset($conf['build_mode'])) {
+    unset($form_state['conf']['teaser']);
+    $conf['build_mode'] = $conf['teaser'] ? 'teaser' : 'full';
+  }
+  $form['build_mode'] = array(
+    '#title' => t('Build mode'),
+    '#type' => 'select',
+    '#description' => t('Select a build mode for this node.'),
+    '#options' => $build_mode_options,
+    '#default_value' => $conf['build_mode'],
+  );
+
 }
 
 /**
@@ -177,7 +223,9 @@ function  ctools_node_content_type_edit_form_validate(&$form, &$form_state) {
     $form_state['values']['nid'] = $node->nid;
   }
 
-  if (!($node || preg_match('/^[@%]\d+$/', $nid))) {
+  if (!($node || preg_match('/^[@%]\d+$/', $nid)) ||
+      // Do not allow unpublished nodes to be selected by unprivileged users
+      (empty($node->status) && !user_access('administer nodes'))) {
     form_error($form['nid'], t('Invalid node'));
   }
 }
@@ -186,7 +234,7 @@ function  ctools_node_content_type_edit_form_validate(&$form, &$form_state) {
  * Validate the node selection.
  */
 function ctools_node_content_type_edit_form_submit(&$form, &$form_state) {
-  foreach (array('nid', 'teaser', 'links', 'leave_node_title', 'identifier') as $key) {
+  foreach (array('nid', 'links', 'leave_node_title', 'link_node_title', 'identifier', 'build_mode') as $key) {
     $form_state['conf'][$key] = $form_state['values'][$key];
   }
 }
@@ -201,7 +249,12 @@ function ctools_node_content_type_admin_title($subtype, $conf) {
 
   $node = node_load($conf['nid']);
   if ($node) {
-    return check_plain($node->title);
+    if (!empty($data->status) || user_access('administer nodes')) {
+      return check_plain($node->title);
+    }
+    else {
+      return t('Unpublished node @nid', array('@nid' => $conf['nid']));
+    }
   }
   else {
     return t('Deleted/missing node @nid', array('@nid' => $conf['nid']));
index 34ba18d..717922b 100644 (file)
@@ -33,11 +33,13 @@ function ctools_node_body_content_type_render($subtype, $conf, $panel_args, $con
     return;
   }
 
+  $body = str_replace('<!--break-->', '', $node->body);
+
   // Build the content type block.
   $block = new stdClass();
   $block->module  = 'node_body';
   $block->title   = $type->body_label;
-  $block->content = check_markup($node->body, $node->format, FALSE);
+  $block->content = check_markup($body, $node->format, FALSE);
   $block->delta   = $node->nid;
 
   return $block;
index 2804cf4..307101a 100644 (file)
@@ -13,6 +13,7 @@ if (module_exists('comment')) {
     'description' => t('A form to add a new comment.'),
     'required context' => new ctools_context_required(t('Node'), 'node'),
     'category' => t('Node'),
+    'defaults' => array('anon_links' => false),
   );
 }
 
@@ -37,6 +38,9 @@ function ctools_node_comment_form_content_type_render($subtype, $conf, $panel_ar
       );
       $block->content = ctools_build_form('comment_form', $form_state);
     }
+    else if (!empty($conf['anon_links'])) {
+      $block->content = theme('comment_post_forbidden', $node);
+    }
   }
 
   return $block;
@@ -47,7 +51,24 @@ function ctools_node_comment_form_content_type_admin_title($subtype, $conf, $con
 }
 
 function ctools_node_comment_form_content_type_edit_form(&$form, &$form_state) {
-  // provide a blank form so we have a place to have context setting.
+  $form['anon_links'] = array(
+    '#type'  => 'checkbox',
+    '#title' => t('Shows links to register or login.'),
+    '#description' => t('If anonymous comments are not allowed, this will display the register and login links.'),
+    '#default_value' => $form_state['conf']['anon_links'],
+  );
+}
+
+function ctools_node_comment_form_content_type_edit_form_submit(&$form, &$form_state) {
+  // For each part of the form defined in the 'defaults' array set when you
+  // defined the content type, copy the value from the form into the array
+  // of items to be saved. We don't ever want to use
+  // $form_state['conf'] = $form_state['values'] because values contains
+  // buttons, form id and other items we don't want stored. CTools will handle
+  // the actual form submission.
+  foreach (array_keys($form_state['plugin']['defaults']) as $key) {
+    $form_state['conf'][$key] = $form_state['values'][$key];
+  }
 }
 
 /**
@@ -55,10 +76,8 @@ function ctools_node_comment_form_content_type_edit_form(&$form, &$form_state) {
  */
 function ctools_form_comment_form_alter(&$form, &$form_state) {
   if (!empty($form_state['ctools comment alter'])) {
-    $node =
-    // force the form to post back to wherever we are.
-    $url = parse_url($_GET['q']);
-    $form['#action'] = url($url['path'], array('fragment' => 'comment-form'));
+    // Force the form to post back to wherever we are.
+    $form['#action'] = url($_GET['q'], array('fragment' => 'comment-form'));
     if (empty($form['#submit'])) {
       $form['#submit'] = array('comment_form_submit');
     }
index 79ac1ef..f1b41e7 100644 (file)
@@ -31,7 +31,7 @@ function ctools_node_comments_content_type_render($subtype, $conf, $panel_args,
   if (empty($node)) {
     $block->content = t('Node comments go here.');
   }
-  else {
+  else if ($node->comment) {
     $block->content = ctools_comment_render($node, $conf);
     // Update the history table, stating that this user viewed this node.
     node_tag_new($node->nid);
@@ -85,7 +85,7 @@ function ctools_node_comments_content_type_admin_title($subtype, $conf, $context
  */
 function ctools_comment_render($node, $conf) {
   $output = '';
-  if (!user_access('access comments')) {
+  if (!user_access('access comments') || !$node->comment) {
     return;
   }
 
index 1e19ca6..e0a6a69 100644 (file)
@@ -18,10 +18,10 @@ $plugin = array(
     'no_extras' => TRUE,
     'override_title' => FALSE,
     'override_title_text' => '',
-    'teaser' => TRUE,
     'identifier' => '',
     'link' => TRUE,
     'leave_node_title' => FALSE,
+    'build_mode' => 'teaser',
   ),
 );
 
@@ -60,8 +60,10 @@ function ctools_node_content_content_type_render($subtype, $conf, $panel_args, $
       'title' => t('Edit node'),
       'alt' => t("Edit this node"),
       'href' => "node/$node->nid/edit",
-      'query' => drupal_get_destination(),
     );
+    if (isset($_REQUEST['destination'])) {
+      $block->admin_links['update']['query'] = drupal_get_destination();
+    }
   }
 
   if (!empty($conf['link']) && $node) {
@@ -72,37 +74,46 @@ function ctools_node_content_content_type_render($subtype, $conf, $panel_args, $
 }
 
 function ctools_node_content_render_node($node, $conf) {
+
+  // Handle existing configurations with the deprecated 'teaser' option.
+  if (isset($conf['teaser'])) {
+    $conf['build_mode'] = $conf['teaser'] ? 'teaser' : 'full';
+  }
+
   // The build mode identifies the target for which the node is built.
   if (!isset($node->build_mode)) {
-    $node->build_mode = NODE_BUILD_NORMAL;
+    $node->build_mode = ($conf['build_mode'] == 'teaser' || $conf['build_mode'] == 'full') ? NODE_BUILD_NORMAL : $conf['build_mode'];
   }
 
+  // Determine the $teaser variable.
+  $teaser = $conf['build_mode'] == 'teaser';
+
   // Remove the delimiter (if any) that separates the teaser from the body.
   $node->body = str_replace('<!--break-->', '', $node->body);
 
   // The 'view' hook can be implemented to overwrite the default function
   // to display nodes.
   if (node_hook($node, 'view')) {
-    $node = node_invoke($node, 'view', $conf['teaser'], $conf['page']);
+    $node = node_invoke($node, 'view', $teaser, $conf['page']);
   }
   else {
-    $node = node_prepare($node, $conf['teaser']);
+    $node = node_prepare($node, $teaser);
   }
 
   if (empty($conf['no_extras'])) {
     // Allow modules to make their own additions to the node.
-    node_invoke_nodeapi($node, 'view', $conf['teaser'], $conf['page']);
+    node_invoke_nodeapi($node, 'view', $teaser, $conf['page']);
   }
 
   if ($conf['links']) {
-    $node->links = module_invoke_all('link', 'node', $node, $conf['teaser']);
+    $node->links = module_invoke_all('link', 'node', $node, $teaser);
     drupal_alter('link', $node->links, $node);
   }
 
   // Set the proper node part, then unset unused $node part so that a bad
   // theme can not open a security hole.
   $content = drupal_render($node->content);
-  if ($conf['teaser']) {
+  if ($teaser) {
     $node->teaser = $content;
     unset($node->body);
   }
@@ -112,9 +123,9 @@ function ctools_node_content_render_node($node, $conf) {
   }
 
   // Allow modules to modify the fully-built node.
-  node_invoke_nodeapi($node, 'alter', $conf['teaser'], $conf['page']);
+  node_invoke_nodeapi($node, 'alter', $teaser, $conf['page']);
 
-  return theme('node', $node, $conf['teaser'], $conf['page']);
+  return theme('node', $node, $teaser, $conf['page']);
 }
 
 /**
@@ -136,11 +147,6 @@ function ctools_node_content_content_type_edit_form(&$form, &$form_state) {
     '#default_value' => $conf['link'],
     '#description' => t('Check here to make the title link to the node.'),
   );
-  $form['teaser'] = array(
-    '#title' => t('Show only node teaser'),
-    '#type' => 'checkbox',
-    '#default_value' => $conf['teaser'],
-  );
   $form['page'] = array(
     '#type' => 'checkbox',
     '#default_value' => $conf['page'],
@@ -167,6 +173,38 @@ function ctools_node_content_content_type_edit_form(&$form, &$form_state) {
     '#description' => t('This identifier will be added as a template suggestion to display this node: node-panel-IDENTIFIER.tpl.php. Please see the Drupal theming guide for information about template suggestions.'),
   );
 
+  // CCK holds the registry of available build modes, but can hardly
+  // push them as options for the build mode options, so we break the normal
+  // rule of not directly relying on non-core modules.
+  if ($modes = module_invoke('content', 'build_modes')) {
+    $build_mode_options = array();
+    foreach ($modes as $key => $value) {
+      if (isset($value['views style']) && $value['views style']) {
+        $build_mode_options[$key] = $value['title'];
+      }
+    }
+  }
+  else {
+    $build_mode_options = array(
+      'teaser' => t('Teaser'),
+      'full' => t('Full node')
+    );
+  }
+
+  // Handle existing configurations with the deprecated 'teaser' option.
+  // Also remove the teaser key from the form_state.
+  if (isset($conf['teaser']) || !isset($conf['build_mode'])) {
+    unset($form_state['conf']['teaser']);
+    $conf['build_mode'] = $conf['teaser'] ? 'teaser' : 'full';
+  }
+  $form['build_mode'] = array(
+    '#title' => t('Build mode'),
+    '#type' => 'select',
+    '#description' => t('Select a build mode for this node.'),
+    '#options' => $build_mode_options,
+    '#default_value' => $conf['build_mode'],
+  );
+
   return $form;
 }
 
index 622fe48..b8da6fd 100644 (file)
@@ -32,7 +32,7 @@ function ctools_node_updated_content_type_render($subtype, $conf, $panel_args, $
   $block = new stdClass();
   $block->module  = 'node_updated';
   $block->title   = t('Last updated date');
-  $block->content = format_date(!empty($node->updated) ? $node->updated : $node->created, $conf['format']);
+  $block->content = format_date(!empty($node->changed) ? $node->changed : $node->created, $conf['format']);
   $block->delta   = $node->nid;
 
   return $block;
index 0a9d28a..d77129b 100644 (file)
@@ -24,7 +24,7 @@ function ctools_node_form_attachments_content_type_render($subtype, $conf, $pane
   $block->delta = 'url-path-options';
 
   if (isset($context->form)) {
-    if (!empty($context->form->form_id) && !empty($context->form['attachments']['#access'])) {
+    if (!empty($context->form['form_id']) && !empty($context->form['attachments']['#access'])) {
       // remove the fieldset
       unset($context->form['attachments']['#type']);
       $block->content = drupal_render($context->form['attachments']);
index 95aa7f8..9fba295 100644 (file)
@@ -18,11 +18,11 @@ function ctools_node_form_author_content_type_render($subtype, $conf, $panel_arg
   $block = new stdClass();
   $block->module = t('node_form');
 
-  $block->title = $block->content = t('Authoring information');
+  $block->title = t('Authoring information');
   $block->delta = 'author-options';
 
   if (isset($context->form)) {
-    if (!empty($context->form->form_id) && !empty($context->form['author']['#access'])) {
+    if (!empty($context->form['form_id']) && !empty($context->form['author']['#access'])) {
       // remove the fieldset
       unset($context->form['author']['#type']);
       $context->form['author']['name']['#size'] /= 2;
index 48faccb..6717e6e 100644 (file)
@@ -24,7 +24,7 @@ function ctools_node_form_book_content_type_render($subtype, $conf, $panel_args,
   $block->delta = 'book-options';
 
   if (isset($context->form)) {
-    if (!empty($context->form->form_id)) {
+    if (!empty($context->form['form_id'])) {
       $block->content = '';
       if ($context->form['parent']['#type'] != 'value') {
         $block->content .= drupal_render($context->form['parent']);
index 88d4fe9..5eab52a 100644 (file)
@@ -23,6 +23,9 @@ function ctools_node_form_buttons_content_type_render($subtype, $conf, $panel_ar
 
   if (isset($context->form)) {
     $block->content = drupal_render($context->form['buttons']);
+    $block->content .= drupal_render($context->form['form_token']);
+    $block->content .= drupal_render($context->form['form_build_id']);
+    $block->content .= drupal_render($context->form['form_id']);
   }
   else {
     $block->content = t('Node form buttons.');
index b9bec9b..7025e58 100644 (file)
@@ -24,7 +24,7 @@ function ctools_node_form_comment_content_type_render($subtype, $conf, $panel_ar
   $block->delta = 'comment-options';
 
   if (isset($context->form)) {
-    if (!empty($context->form->form_id) && !empty($context->form['comment_settings']['#access'])) {
+    if (!empty($context->form['form_id']) && !empty($context->form['comment_settings']['#access'])) {
       // remove the fieldset
       unset($context->form['comment_settings']['#type']);
       $block->content = drupal_render($context->form['comment_settings']);
index a1fdc49..18d1a57 100644 (file)
@@ -22,7 +22,7 @@ function ctools_node_form_input_format_content_type_render($subtype, $conf, $pan
   $block->delta = 'format-options';
 
   if (isset($context->form)) {
-    if (!empty($context->form->form_id) && !empty($context->form['body_filter']['format'])) {
+    if (!empty($context->form['form_id']) && !empty($context->form['body_filter']['format'])) {
       // remove the fieldset
       unset($context->form['body_filter']['format']['#type']);
       $block->content = drupal_render($context->form['body_filter']['format']);
index 7467f19..3c49b3a 100644 (file)
@@ -24,7 +24,7 @@ function ctools_node_form_menu_content_type_render($subtype, $conf, $panel_args,
   $block->delta = 'menu-options';
 
   if (isset($context->form)) {
-    if (!empty($context->form->form_id) && !empty($context->form['menu']['#access'])) {
+    if (!empty($context->form['form_id']) && !empty($context->form['menu']['#access'])) {
       // remove the fieldset
       unset($context->form['menu']['#type']);
       $context->form['menu']['link_title']['#size'] /= 2;
index b74d098..1d4d570 100644 (file)
@@ -24,7 +24,7 @@ function ctools_node_form_path_content_type_render($subtype, $conf, $panel_args,
   $block->delta = 'url-path-options';
 
   if (isset($context->form)) {
-    if (!empty($context->form->form_id) && !empty($context->form['path']['#access'])) {
+    if (!empty($context->form['form_id']) && !empty($context->form['path']['#access'])) {
       // remove the fieldset
       unset($context->form['path']['#type']);
       $context->form['path']['path']['#size'] /= 2;
index 008fd3a..4a3972a 100644 (file)
@@ -28,7 +28,7 @@ function ctools_node_form_publishing_content_type_render($subtype, $conf, $panel
   $block->delta = 'publishing-options';
 
   if (isset($context->form)) {
-    if (!empty($context->form->form_id) && $context->form['options']['#type'] == 'fieldset') {
+    if (!empty($context->form['form_id']) && $context->form['options']['#type'] == 'fieldset') {
       // remove the fieldset
       unset($context->form['options']['#type']);
       $block->content = drupal_render($context->form['options']);
index 693b552..1cb26bc 100644 (file)
@@ -24,7 +24,7 @@ function ctools_node_form_taxonomy_content_type_render($subtype, $conf, $panel_a
       $block->delta = 'url-path-options';
 
       if (isset($context->form)) {
-        if (!empty($context->form->form_id) && !empty($context->form['taxonomy'])) {
+        if (!empty($context->form['form_id']) && !empty($context->form['taxonomy'])) {
           // remove the fieldset
           unset($context->form['taxonomy']['#type']);
           $block->content = drupal_render($context->form['taxonomy']);
index b913b4f..46e0d6d 100644 (file)
@@ -8,17 +8,17 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_page_breadcrumb_ctools_content_types() {
-  return array(
-    'title' => t('Breadcrumb'),
-    'single' => TRUE,
-    'icon' => 'icon_page.png',
-    'description' => t('Add the breadcrumb trail as content.'),
-    'category' => t('Page elements'),
-  );
-}
+$plugin = array(
+  'title' => t('Breadcrumb'),
+  'single' => TRUE,
+  'icon' => 'icon_page.png',
+  'description' => t('Add the breadcrumb trail as content.'),
+  'category' => t('Page elements'),
+  'render last' => TRUE,
+);
 
 /**
  * Output function for the 'page_breadcrumb' content type.
@@ -26,12 +26,8 @@ function ctools_page_breadcrumb_ctools_content_types() {
  * Outputs the breadcrumb for the current page.
  */
 function ctools_page_breadcrumb_content_type_render($subtype, $conf, $panel_args) {
-  $token = '<!-- ctools-page-breadcrumb -->';
-  ctools_set_page_token($token, 'variable', 'breadcrumb');
-
   $block = new stdClass();
-  $block->content = $token;
+  $block->content = theme('breadcrumb', drupal_get_breadcrumb());
 
   return $block;
 }
-
index 3503e9c..074a30a 100644 (file)
@@ -8,17 +8,17 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_page_footer_message_ctools_content_types() {
-  return array(
-    'single' => TRUE,
-    'title' => t('Page footer message'),
-    'icon' => 'icon_page.png',
-    'description' => t('Add the page footer message as content.'),
-    'category' => t('Page elements'),
-  );
-}
+$plugin = array(
+  'single' => TRUE,
+  'title' => t('Page footer message'),
+  'icon' => 'icon_page.png',
+  'description' => t('Add the page footer message as content.'),
+  'category' => t('Page elements'),
+  'render last' => TRUE,
+);
 
 /**
  * Output function for the 'page_footer_message' content type.
@@ -26,11 +26,8 @@ function ctools_page_footer_message_ctools_content_types() {
  * Outputs the page footer message of the current page.
  */
 function ctools_page_footer_message_content_type_render($subtype, $conf, $panel_args) {
-  $token = '<!-- ctools-page-footer-message -->';
-  ctools_set_page_token($token, 'variable', 'footer_message');
-
   $block = new stdClass();
-  $block->content = $token;
+  $block->content = filter_xss_admin(variable_get('site_footer', FALSE));
 
   return $block;
 }
\ No newline at end of file
index b2197d5..1a81151 100644 (file)
@@ -8,17 +8,17 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_page_help_ctools_content_types() {
-  return array(
-    'title' => t('Help'),
-    'single' => TRUE,
-    'icon' => 'icon_page.png',
-    'description' => t('Add the help text of the current page as content.'),
-    'category' => t('Page elements'),
-  );
-}
+$plugin = array(
+  'title' => t('Help'),
+  'single' => TRUE,
+  'icon' => 'icon_page.png',
+  'description' => t('Add the help text of the current page as content.'),
+  'category' => t('Page elements'),
+  'render last' => TRUE,
+);
 
 /**
  * Output function for the 'page_help' content type.
@@ -26,11 +26,8 @@ function ctools_page_help_ctools_content_types() {
  * Outputs the breadcrumb for the current page.
  */
 function ctools_page_help_content_type_render($subtype, $conf, $panel_args) {
-  $token = '<!-- ctools-page-help -->';
-  ctools_set_page_token($token, 'variable', 'help');
-
   $block = new stdClass();
-  $block->content = $token;
+  $block->content = theme('help');
 
   return $block;
 }
index f1fc183..135cb7f 100644 (file)
@@ -8,17 +8,17 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_page_messages_ctools_content_types() {
-  return array(
-    'title' => t('Status messages'),
-    'single' => TRUE,
-    'icon' => 'icon_page.png',
-    'description' => t('Add the status messages of the current page as content.'),
-    'category' => t('Page elements'),
-  );
-}
+$plugin = array(
+  'title' => t('Status messages'),
+  'single' => TRUE,
+  'icon' => 'icon_page.png',
+  'description' => t('Add the status messages of the current page as content.'),
+  'category' => t('Page elements'),
+  'render last' => TRUE,
+);
 
 /**
  * Output function for the 'page_messages' content type.
@@ -26,11 +26,8 @@ function ctools_page_messages_ctools_content_types() {
  * Outputs the breadcrumb for the current page.
  */
 function ctools_page_messages_content_type_render($subtype, $conf, $panel_args) {
-  $token = '<!-- ctools-page-messages -->';
-  ctools_set_page_token($token, 'variable', 'messages');
-
   $block = new stdClass();
-  $block->content = $token;
+  $block->content = theme('status_messages');
 
   return $block;
 }
index b6b26a4..ce9dc05 100644 (file)
@@ -8,17 +8,17 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_page_mission_ctools_content_types() {
-  return array(
-    'title' => t('Mission'),
-    'single' => TRUE,
-    'icon' => 'icon_page.png',
-    'description' => t('Add the site mission statement as content.'),
-    'category' => t('Page elements'),
-  );
-}
+$plugin = array(
+  'title' => t('Mission'),
+  'single' => TRUE,
+  'icon' => 'icon_page.png',
+  'description' => t('Add the site mission statement as content.'),
+  'category' => t('Page elements'),
+  'render last' => TRUE,
+);
 
 /**
  * Output function for the 'page_mission' content type.
@@ -26,11 +26,8 @@ function ctools_page_mission_ctools_content_types() {
  * Outputs the mission statement for the site.
  */
 function ctools_page_mission_content_type_render($subtype, $conf, $panel_args) {
-  $token = '<!-- ctools-page-mission -->';
-  ctools_set_page_token($token, 'variable', 'mission');
-
   $block = new stdClass();
-  $block->content = $token;
+  $block->content = filter_xss_admin(theme_get_setting('mission'));
 
   return $block;
 }
index f18d9bd..6d3cf95 100644 (file)
@@ -8,17 +8,17 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_page_slogan_ctools_content_types() {
-  return array(
-    'title' => t('Site Slogan'),
-    'single' => TRUE,
-    'icon' => 'icon_page.png',
-    'description' => t('Add the slogan trail as content.'),
-    'category' => t('Page elements'),
-  );
-}
+$plugin = array(
+  'title' => t('Site slogan'),
+  'single' => TRUE,
+  'icon' => 'icon_page.png',
+  'description' => t('Add the slogan trail as content.'),
+  'category' => t('Page elements'),
+  'render last' => TRUE,
+);
 
 /**
  * Output function for the 'page_slogan' content type.
@@ -26,11 +26,8 @@ function ctools_page_slogan_ctools_content_types() {
  * Outputs the slogan for the current page.
  */
 function ctools_page_slogan_content_type_render($subtype, $conf, $panel_args) {
-  $token = '<!-- ctools-page-slogan -->';
-  ctools_set_page_token($token, 'variable', 'slogan');
-
   $block = new stdClass();
-  $block->content = $token;
+  $block->content = (theme_get_setting('toggle_slogan') ? filter_xss_admin(variable_get('site_slogan', '')) : '');
 
   return $block;
 }
index 68d0cee..4b42c91 100644 (file)
@@ -8,17 +8,21 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_page_tabs_ctools_content_types() {
-  return array(
-    'title' => t('Tabs'),
-    'single' => TRUE,
-    'icon' => 'icon_page.png',
-    'description' => t('Add the tabs (local tasks) as content.'),
-    'category' => t('Page elements'),
-  );
-}
+$plugin = array(
+  'title' => t('Tabs'),
+  'single' => TRUE,
+  'icon' => 'icon_page.png',
+  'description' => t('Add the tabs (local tasks) as content.'),
+  'category' => t('Page elements'),
+  'render last' => TRUE,
+  'defaults' => array(
+    'type' => 'both',
+    'id' => 'tabs',
+  ),
+);
 
 /**
  * Output function for the 'page_tabs' content type.
@@ -26,12 +30,41 @@ function ctools_page_tabs_ctools_content_types() {
  * Outputs the tabs (local tasks) of the current page.
  */
 function ctools_page_tabs_content_type_render($subtype, $conf, $panel_args) {
-  $token = '<!-- ctools-page-tabs -->';
-  ctools_set_page_token($token, 'variable', 'tabs');
-
   $block = new stdClass();
-  $block->content = $token;
+  $block->content = theme('menu_local_tasks');
 
   return $block;
 }
 
+
+function ctools_page_tabs_content_type_edit_form(&$form, &$form_state) {
+  $conf = $form_state['conf'];
+
+  $form['type'] = array(
+    '#title' => t('Tabs type'),
+    '#type' => 'select',
+    '#options' => array(
+      'both' => t('Primary and secondary'),
+      'primary' => t('Primary'),
+      'secondary' => t('Secondary'),
+    ),
+    '#default_value' => $conf['type'],
+  );
+
+  $form['id'] = array(
+    '#title' => t('CSS id to use'),
+    '#type' => 'textfield',
+    '#default_value' => $conf['id'],
+  );
+}
+
+/**
+ * The submit form stores the data in $conf.
+ */
+function ctools_page_tabs_content_type_edit_form_submit(&$form, &$form_state) {
+  foreach (array_keys($form_state['plugin']['defaults']) as $key) {
+    if (isset($form_state['values'][$key])) {
+      $form_state['conf'][$key] = $form_state['values'][$key];
+    }
+  }
+}
index bf37616..a1fa290 100644 (file)
@@ -8,17 +8,21 @@
  */
 
 /**
- * Implements hook_ctools_content_types()
+ * Plugins are described by creating a $plugin array which will be used
+ * by the system that includes this file.
  */
-function ctools_page_title_ctools_content_types() {
-  return array(
-    'single' => TRUE,
-    'title' => t('Page title'),
-    'icon' => 'icon_page.png',
-    'description' => t('Add the page title as content.'),
-    'category' => t('Page elements'),
-  );
-}
+$plugin = array(
+  'single' => TRUE,
+  'title' => t('Page title'),
+  'icon' => 'icon_page.png',
+  'description' => t('Add the page title as content.'),
+  'category' => t('Page elements'),
+  'defaults' => array(
+    'markup' => 'h1',
+    'class' => '',
+    'id' => '',
+  ),
+);
 
 /**
  * Output function for the 'page_title' content type.
@@ -26,11 +30,89 @@ function ctools_page_title_ctools_content_types() {
  * Outputs the page title of the current page.
  */
 function ctools_page_title_content_type_render($subtype, $conf, $panel_args) {
-  $token = '<!-- ctools-page-title -->';
-  ctools_set_page_token($token, 'variable', 'title');
+  // TODO: This should have a setting or something for the markup.
+  if (empty($conf['markup'])) {
+    $conf['markup'] = 'h1';
+  }
+
+  if (empty($conf['class'])) {
+    $conf['class'] = '';
+  }
+
+  if (empty($conf['id'])) {
+    $conf['id'] = '';
+  }
+
+  $token = ctools_set_callback_token('title', array('ctools_page_title_content_type_token', $conf['markup'], $conf['id'], $conf['class']));
 
   $block = new stdClass();
-  $block->content = '<h1>'. $token .'</h1>';
+  if ($token) {
+    $block->content = $token;
+  }
 
   return $block;
-}
\ No newline at end of file
+}
+
+function ctools_page_title_content_type_edit_form(&$form, &$form_state) {
+  $conf = $form_state['conf'];
+
+  $form['markup'] = array(
+    '#title' => t('Title tag'),
+    '#type' => 'select',
+    '#options' => array(
+      'none' => t('- No tag -'),
+      'h1' => t('h1'),
+      'h2' => t('h2'),
+      'h3' => t('h3'),
+      'h4' => t('h4'),
+      'h5' => t('h5'),
+      'h6' => t('h6'),
+      'div' => t('div'),
+    ),
+    '#default_value' => empty($conf['markup']) ? 'h1' : $conf['markup'],
+  );
+
+  $form['id'] = array(
+    '#title' => t('CSS id to use'),
+    '#type' => 'textfield',
+    '#default_value' => empty($conf['id']) ? '' : $conf['id'],
+  );
+
+  $form['class'] = array(
+    '#title' => t('CSS class to use'),
+    '#type' => 'textfield',
+    '#default_value' => empty($conf['class']) ? '' : $conf['class'],
+  );
+}
+
+/**
+ * The submit form stores the data in $conf.
+ */
+function ctools_page_title_content_type_edit_form_submit(&$form, &$form_state) {
+  foreach (array_keys($form_state['plugin']['defaults']) as $key) {
+    if (isset($form_state['values'][$key])) {
+      $form_state['conf'][$key] = $form_state['values'][$key];
+    }
+  }
+}
+
+/**
+ * Variable token callback to properly render the page title, with markup.
+ */
+function ctools_page_title_content_type_token(&$variables, $tag, $id, $class) {
+  if ($tag == 'none') {
+    return drupal_get_title();
+  }
+
+  $output = '<' . $tag;
+  if ($id) {
+    $output .= ' id="' . $id . '"';
+  }
+
+  if ($class) {
+    $output .= ' class="' . $class . '"';
+  }
+
+  $output .= '>' . drupal_get_title() . '</' . $tag . '>' . "\n";
+  return $output;
+}
index 88a6c12..7d8c551 100644 (file)
@@ -1,14 +1,12 @@
 <?php
 // $Id$
 
-/**
- * Callback function to supply a list of content types.
- */
-function ctools_search_result_ctools_content_types() {
-  if (!module_exists('search')) {
-    return;
-  }
-  return array(
+if (module_exists('search')) {
+  /**
+   * Plugins are described by creating a $plugin array which will be used
+   * by the system that includes this file.
+   */
+  $plugin = array(
     'single' => TRUE,
     'title' => t('Search results'),
     'icon' => 'icon_search.png',
@@ -88,6 +86,12 @@ function ctools_search_result_content_type_render($subtype, $conf, $panel_args,
 function ctools_search_result_content_type_edit_form(&$form, &$form_state) {
   $conf = $form_state['conf'];
 
+<<<<<<< HEAD
+=======
+  // Add js for collapsible fieldsets manually
+  drupal_add_js('misc/collapse.js');
+
+>>>>>>> DRUPAL-6--1
   $types = array();
   foreach (module_implements('search') as $name) {
     $types[$name] = module_invoke($name, 'search', 'name', TRUE);
index bcfba11..645045f 100644 (file)
@@ -52,7 +52,12 @@ function ctools_term_list_content_type_render($subtype, $conf, $panel_args, $con
     }
     if ($terms) {
       foreach ($terms as $related) {
-        $items[$related->tid] = l($related->name, taxonomy_term_path($related), array('rel' => 'tag', 'title' => strip_tags($related->description)));
+        if (is_object($related)) {
+          $items[] = l($related->name, taxonomy_term_path($related), array('rel' => 'tag', 'title' => strip_tags($related->description)));
+        }
+        else {
+          $items[] = check_plain($related);
+        }
       }
 
       $block->content = theme('item_list', $items, NULL, $conf['list_type']);
index 25b958f..f7d5cda 100644 (file)
@@ -138,7 +138,8 @@ function ctools_context_node_settings_form_validate($form, &$form_values, &$form
     $node = db_query('SELECT nid FROM {node} WHERE LOWER(title) = LOWER(:title)', array(':title' => $nid))->fetchObject();
   }
 
-  if (!$node) {
+  // Do not allow unpublished nodes to be selected by unprivileged users
+  if (!$node || (empty($node->status) && !(user_access('administer nodes')))) {
     form_error($form['node'], t('Invalid node selected.'));
   }
   else {
@@ -161,13 +162,19 @@ function ctools_context_node_settings_form_submit($form, &$form_values, &$form_s
  * Provide a list of ways that this context can be converted to a string.
  */
 function ctools_context_node_convert_list() {
-  return array(
+  $list = array(
     'nid' => t('Node ID'),
     'vid' => t('Node revision ID'),
     'title' => t('Node title'),
     'uid' => t('Author UID'),
     'type' => t('Node type'),
   );
+
+  if (module_exists('token')) {
+    $list += reset(token_get_list(array('node')));
+  }
+
+  return $list;
 }
 
 /**
@@ -186,4 +193,10 @@ function ctools_context_node_convert($context, $type) {
     case 'type':
       return $context->data->type;
   }
+  if (module_exists('token')) {
+    $values = token_get_values('node', $context->data);
+    if ($key = array_search($type, $values->tokens)) {
+      return $values->values[$key];
+    }
+  }
 }
index a4c78be..69c87d7 100644 (file)
@@ -31,12 +31,14 @@ $plugin = array(
  * are not always created from the UI.
  */
 function ctools_context_create_node_add_form($empty, $data = NULL, $conf = FALSE) {
+  static $created;
   $context = new ctools_context(array('form', 'node_add', 'node_form'));
   $context->plugin = 'node_add_form';
 
-  if ($empty) {
+  if ($empty || (isset($created) && $created)) {
     return $context;
   }
+  $created = TRUE;
 
   if ($conf && (isset($data['types']) || isset($data['type']))) {
     // Holdover from typo'd config.
index 34542d5..84373a4 100644 (file)
@@ -35,12 +35,14 @@ $plugin = array(
  * are not always created from the UI.
  */
 function ctools_context_create_node_edit_form($empty, $node = NULL, $conf = FALSE) {
-  $context = new ctools_context(array('form', 'node_edit', 'node_form', 'node'));
+  static $created;
+  $context = new ctools_context(array('form', 'node_edit', 'node_form', 'node', 'node_edit_form'));
   $context->plugin = 'node_edit_form';
 
-  if ($empty) {
+  if ($empty || (isset($created) && $created)) {
     return $context;
   }
+  $created = TRUE;
 
   if ($conf) {
     // In this case, $node is actually our $conf array.
@@ -180,7 +182,7 @@ function ctools_context_node_edit_form_settings_form_submit($form, &$form_values
 /**
  * Provide a list of ways that this context can be converted to a string.
  */
-function ctools_context_node_convert_edit_list() {
+function ctools_context_node_edit_convert_list() {
   // Pass through to the "node" context convert list.
   $plugin = ctools_get_context('node');
   return ctools_context_node_convert_list();
@@ -189,7 +191,7 @@ function ctools_context_node_convert_edit_list() {
 /**
  * Convert a context into a string.
  */
-function ctools_context_node_convert($context, $type) {
+function ctools_context_node_edit_convert($context, $type) {
   // Pass through to the "node" context convert list.
   $plugin = ctools_get_context('node');
   return ctools_context_node_convert($context, $type);
index 23081ad..c15a8d4 100644 (file)
@@ -23,6 +23,7 @@ $plugin = array(
   'convert list' => array(
     'tid' => t('Term ID'),
     'name' => t('Term name'),
+    'description' => t('Term Description'),
     'vid' => t('Vocabulary ID'),
   ),
   'convert' => 'ctools_context_term_convert',
@@ -44,11 +45,12 @@ function ctools_context_create_term($empty, $data = NULL, $conf = FALSE) {
   if ($conf && isset($data['tid'])) {
     $data = taxonomy_term_load($data['tid']);
   }
-
+  
   if (!empty($data)) {
-    $context->data     = $data;
-    $context->title    = $data->name;
-    $context->argument = $data->tid;
+    $context->data        = $data;
+    $context->title       = $data->name;
+    $context->argument    = $data->tid;
+    $context->description = $data->description;
     return $context;
   }
 }
@@ -59,6 +61,7 @@ function ctools_context_term_settings_form($conf) {
       'vid' => '',
       'tid' => '',
       'term' => '',
+      'description' => '',
     );
   }
 
@@ -163,5 +166,7 @@ function ctools_context_term_convert($context, $type) {
       return $context->data->name;
     case 'vid':
       return $context->data->vid;
+    case 'description':
+      return $context->data->description;
   }
 }
index 8bede1f..63acafc 100644 (file)
@@ -20,13 +20,11 @@ $plugin = array(
   'settings form submit' => 'ctools_context_user_settings_form_submit',
   'keywor