From b44374f69f1488b6ddc0e0af047a526cd9852e51 Mon Sep 17 00:00:00 2001 From: boshtian Date: Mon, 26 Feb 2018 14:07:25 +0100 Subject: [PATCH] Revert "Clean up Drupal 7 code, prepare for Drupal 8 push." This reverts commit a1cedf59171415ef4933480fb288eaea40961cc4. --- API.txt | 106 +++ README.txt | 84 +++ context.api.php | 141 ++++ context.core.inc | 385 ++++++++++ context.info | 9 + context.install | 112 +++ context.module | 569 +++++++++++++++ context.plugins.inc | 392 ++++++++++ context_layouts/README.txt | 85 +++ context_layouts/context_layouts.info | 7 + context_layouts/context_layouts.module | 132 ++++ .../context_layouts_reaction_block.css | 8 + .../context_layouts_reaction_block.inc | 205 ++++++ .../plugins/context_layouts_reaction_block.js | 27 + context_ui/README.txt | 73 ++ context_ui/context_ui.css | 182 +++++ context_ui/context_ui.info | 9 + context_ui/context_ui.install | 9 + context_ui/context_ui.js | 148 ++++ context_ui/context_ui.module | 441 ++++++++++++ context_ui/context_ui_dialog.css | 107 +++ context_ui/context_ui_dialog.js | 47 ++ context_ui/export_ui/context.inc | 24 + .../export_ui/context_export_ui.class.php | 327 +++++++++ context_ui/jquery.pageEditor.js | 38 + context_ui/json2.js | 1 + context_ui/tests/context_ui.test | 61 ++ context_ui/theme/context-ui-editor.tpl.php | 10 + context_ui/theme/context-ui-form.tpl.php | 2 + context_ui/theme/context-ui-plugins.tpl.php | 22 + context_ui/theme/filter.js | 66 ++ context_ui/theme/theme.inc | 54 ++ plugins/context_condition.inc | 196 +++++ plugins/context_condition_book.inc | 22 + plugins/context_condition_bookroot.inc | 19 + plugins/context_condition_context.inc | 65 ++ plugins/context_condition_context_all.inc | 29 + plugins/context_condition_default.inc | 35 + plugins/context_condition_language.inc | 16 + plugins/context_condition_menu.inc | 80 ++ plugins/context_condition_node.inc | 62 ++ plugins/context_condition_node_taxonomy.inc | 73 ++ plugins/context_condition_path.inc | 116 +++ plugins/context_condition_query_string.inc | 28 + plugins/context_condition_sitewide.inc | 23 + plugins/context_condition_taxonomy_term.inc | 47 ++ plugins/context_condition_user.inc | 31 + plugins/context_condition_user_page.inc | 59 ++ plugins/context_condition_views.inc | 48 ++ plugins/context_reaction.inc | 86 +++ plugins/context_reaction_block.css | 262 +++++++ plugins/context_reaction_block.inc | 681 ++++++++++++++++++ plugins/context_reaction_block.js | 496 +++++++++++++ plugins/context_reaction_block.png | Bin 0 -> 2677 bytes plugins/context_reaction_block.svg | 256 +++++++ plugins/context_reaction_breadcrumb.inc | 39 + plugins/context_reaction_css_injector.inc | 33 + plugins/context_reaction_debug.inc | 46 ++ plugins/context_reaction_menu.inc | 143 ++++ plugins/context_reaction_region.inc | 46 ++ .../context_reaction_template_suggestions.inc | 47 ++ plugins/context_reaction_theme.inc | 67 ++ plugins/context_reaction_theme_html.inc | 34 + tests/context.conditions.test | 561 +++++++++++++++ tests/context.reactions.test | 311 ++++++++ tests/context.test | 89 +++ theme/context-block-browser-item.tpl.php | 4 + theme/context-block-browser.tpl.php | 21 + theme/context_reaction_block.theme.inc | 136 ++++ 69 files changed, 8190 insertions(+) create mode 100644 API.txt create mode 100644 README.txt create mode 100644 context.api.php create mode 100644 context.core.inc create mode 100644 context.info create mode 100644 context.install create mode 100644 context.module create mode 100644 context.plugins.inc create mode 100644 context_layouts/README.txt create mode 100644 context_layouts/context_layouts.info create mode 100644 context_layouts/context_layouts.module create mode 100644 context_layouts/plugins/context_layouts_reaction_block.css create mode 100644 context_layouts/plugins/context_layouts_reaction_block.inc create mode 100644 context_layouts/plugins/context_layouts_reaction_block.js create mode 100644 context_ui/README.txt create mode 100644 context_ui/context_ui.css create mode 100644 context_ui/context_ui.info create mode 100644 context_ui/context_ui.install create mode 100644 context_ui/context_ui.js create mode 100644 context_ui/context_ui.module create mode 100644 context_ui/context_ui_dialog.css create mode 100644 context_ui/context_ui_dialog.js create mode 100644 context_ui/export_ui/context.inc create mode 100644 context_ui/export_ui/context_export_ui.class.php create mode 100644 context_ui/jquery.pageEditor.js create mode 100644 context_ui/json2.js create mode 100644 context_ui/tests/context_ui.test create mode 100644 context_ui/theme/context-ui-editor.tpl.php create mode 100644 context_ui/theme/context-ui-form.tpl.php create mode 100644 context_ui/theme/context-ui-plugins.tpl.php create mode 100644 context_ui/theme/filter.js create mode 100644 context_ui/theme/theme.inc create mode 100644 plugins/context_condition.inc create mode 100644 plugins/context_condition_book.inc create mode 100644 plugins/context_condition_bookroot.inc create mode 100644 plugins/context_condition_context.inc create mode 100644 plugins/context_condition_context_all.inc create mode 100644 plugins/context_condition_default.inc create mode 100644 plugins/context_condition_language.inc create mode 100644 plugins/context_condition_menu.inc create mode 100644 plugins/context_condition_node.inc create mode 100644 plugins/context_condition_node_taxonomy.inc create mode 100644 plugins/context_condition_path.inc create mode 100644 plugins/context_condition_query_string.inc create mode 100644 plugins/context_condition_sitewide.inc create mode 100644 plugins/context_condition_taxonomy_term.inc create mode 100644 plugins/context_condition_user.inc create mode 100644 plugins/context_condition_user_page.inc create mode 100644 plugins/context_condition_views.inc create mode 100644 plugins/context_reaction.inc create mode 100644 plugins/context_reaction_block.css create mode 100644 plugins/context_reaction_block.inc create mode 100644 plugins/context_reaction_block.js create mode 100644 plugins/context_reaction_block.png create mode 100644 plugins/context_reaction_block.svg create mode 100644 plugins/context_reaction_breadcrumb.inc create mode 100644 plugins/context_reaction_css_injector.inc create mode 100644 plugins/context_reaction_debug.inc create mode 100644 plugins/context_reaction_menu.inc create mode 100644 plugins/context_reaction_region.inc create mode 100644 plugins/context_reaction_template_suggestions.inc create mode 100644 plugins/context_reaction_theme.inc create mode 100644 plugins/context_reaction_theme_html.inc create mode 100644 tests/context.conditions.test create mode 100644 tests/context.reactions.test create mode 100644 tests/context.test create mode 100644 theme/context-block-browser-item.tpl.php create mode 100644 theme/context-block-browser.tpl.php create mode 100644 theme/context_reaction_block.theme.inc diff --git a/API.txt b/API.txt new file mode 100644 index 0000000..9c7142c --- /dev/null +++ b/API.txt @@ -0,0 +1,106 @@ + +Context 3.x API +--------------- +The following is an overview of using the Context API. + + +The context static cache +------------------------ +Context provides a centralized set of API functions for setting and retrieving a +static cache: + + // Set a static cache value at [my_namspace][mykey] + context_set('my_namespace', 'mykey', $value); + + // Retrieve a static cache value at [my_namespace][mykey] + context_get('my_namespace', 'mykey'); // $value + + // Boolean for whether there is a value at [my_namespace][mykey] + context_isset('my_namespace', 'mykey'); // TRUE + +These are used internally by context but may also be used by other modules. Just +do not use the namespace `context` unless you want to affect things that context +is up to. + + +Adding a condition or reaction plugin +------------------------------------- +Both context conditions and reactions utilize the CTools plugins API. In order +to add a new condition or reaction for your module, follow these steps: + +1. Implement `hook_context_plugins()` to define your plugins, classes, and class + hierarchy. + + function mymodule_context_plugins() { + $plugins = array(); + $plugins['mymodule_context_condition_bar'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'mymodule') .'/plugins', + 'file' => 'mymodule_context_condition_bar.inc', + 'class' => 'mymodule_context_condition_bar', + 'parent' => 'context_condition', + ), + ); + return $plugins; + } + +2. Implement `hook_context_registry()` to define your conditions and/or + reactions and map them to plugins. + + function mymodule_context_registry() { + return array( + 'conditions' => array( + 'bar' => array( + 'title' => t('Name of condition "bar"'), + 'plugin' => 'mymodule_context_condition_bar', + ), + ), + ); + } + +3. Write your condition or reaction plugin class. It's best to look at one of + the included plugins as a starting point. + +4. Create a Drupal integration point for your plugin. A node page condition + plugin, for example, may be invoked from `hook_node_view()`. Typically a + Drupal integration point for a condition uses a Drupal hook to trigger + tests that determine whether context conditions are met for one or more + plug-ins. For example, this is how the context module itself uses + hook_init(): + + function context_init() { + if ($plugin = context_get_plugin('condition', 'path')) { + $plugin->execute(); + } + if ($plugin = context_get_plugin('condition', 'language')) { + global $language; + $plugin->execute($language->language); + } + if ($plugin = context_get_plugin('condition', 'user')) { + global $user; + $plugin->execute($user); + } + } + + This function first instantiates the Context module's path condition + plugin (filename context_condition_path.inc in the plugins directory), + and then uses that plugin's execute() method. The execute() method + determine whether any path conditions are met and, if so, it activates + the contexts that match those conditions. After setting contexts based + path conditions, the context_init() function then does the same thing + with the Context module's language and user condition plugins. + +Replacing or extending existing plugins +--------------------------------------- +You can replace a condition or reaction plugin with your own plugin class using +`hook_context_registry_alter()`: + + function mymodule_context_registry_alter(&$registry) { + if (!empty($registry['conditions']['node'])) { + $registry['conditions']['node']['plugin'] = 'mymodule_context_condition_customnode'; + } + } + +This entry would swap out the default node condition plugin for a custom one +provided by `mymodule`. Note that any replacement plugins must have an entry in +`hook_context_plugins()`. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..994ddab --- /dev/null +++ b/README.txt @@ -0,0 +1,84 @@ + +Current state of Context for Drupal 7 +------------------------------------- +Context for D7 is a straight port of Context 3.x from D6. There are no major +API changes and any exported contexts from D6 should be compatible with the D7 +version. You will need the latest CTools (as of Sept. 16 2010) from here: + +- http://github.com/sdboyer/ctools + +### Working + +- all conditions except node taxonomy condition +- all reactions +- context UI +- context layouts +- inline editor (see the context_ui README file for info on enabling) + +### Expect API changes + +- node taxonomy condition to generic field condition for entities + + +Context 3.x for Drupal 7.x +-------------------------- +Context allows you to manage contextual conditions and reactions for +different portions of your site. You can think of each context as +representing a "section" of your site. For each context, you can choose +the conditions that trigger this context to be active and choose different +aspects of Drupal that should react to this active context. + +Think of conditions as a set of rules that are checked during page load +to see what context is active. Any reactions that are associated with +active contexts are then fired. + + +Installation +------------ +Context can be installed like any other Drupal module -- place it in +the modules directory for your site and enable it (and its requirement, +CTools) on the `admin/modules` page. + +You will probably also want to install Context UI which provides a way for +you to edit contexts through the Drupal admin interface. + + +Example +------- +You want to create a "pressroom" section of your site. You have a press +room view that displays press release nodes, but you also want to tie +a book with media resources tightly to this section. You would also +like a contact block you've made to appear whenever a user is in the +pressroom section. + +1. Add a new context on admin/structure/context +2. Under "Conditions", associate the pressroom nodetype, the pressroom view, + and the media kit book with the context. +3. Under "Reactions > Menu", choose the pressroom menu item to be set active. +4. Under "Reactions > Blocks", add the contact block to a region. +5. Save the context. + +For a more in-depth overview of the UI components, see the Context UI +`README.txt`. + + +Hooks +----- +See `context.api.php` for the hooks made available by context and `API.txt` for +usage examples. + + +Maintainers +----------- + +- yhahn (Young Hahn) +- jmiccolis (Jeff Miccolis) +- Steven Jones + + +Contributors +------------ + +- alex_b (Alex Barth) +- dmitrig01 (Dmitri Gaskin) +- Pasqualle (Csuthy Bálint) diff --git a/context.api.php b/context.api.php new file mode 100644 index 0000000..30cff79 --- /dev/null +++ b/context.api.php @@ -0,0 +1,141 @@ + array( + 'path' => drupal_get_path('module', 'foo') .'/plugins', + 'file' => 'foo_context_condition_bar.inc', + 'class' => 'foo_context_condition_bar', + 'parent' => 'context_condition', + ), + ); + $plugins['foo_context_reaction_baz'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'foo') .'/plugins', + 'file' => 'foo_context_reaction_baz.inc', + 'class' => 'foo_context_reaction_baz', + 'parent' => 'context_reaction', + ), + ); + return $plugins; +} + +/** + * Registry hook for conditions & reactions. + * + * Each entry associates a condition or reaction with the CTools plugin to be + * used as its plugin class. + */ +function hook_context_registry() { + return array( + 'conditions' => array( + 'bar' => array( + 'title' => t('Name of condition "bar"'), + 'plugin' => 'foo_context_condition_bar', + ), + ), + 'reactions' => array( + 'baz' => array( + 'title' => t('Name of reaction "baz"'), + 'plugin' => 'foo_context_reaction_baz', + ), + ), + ); +} + +/** + * Execute Context page conditions + * + * Allows modules to hook into Context's hook_page_build to execute their + * conditions at an appropriate time before the firing of reactions. + */ +function hook_context_page_condition() { + if ($plugin = context_get_plugin('condition', 'bar')) { + $plugin->execute(); + } +} + +/** + * Execute Context page reactions + * + * Allows modules to hook into Context's hook_page_build to execute their + * reactions at an appropriate time after the firing of conditions. + */ +function hook_context_page_reaction() { + if ($plugin = context_get_plugin('reaction', 'baz')) { + $plugin->execute(); + } +} + +/** + * Alter the registry. + * + * Allows modules to alter the registry. Default plugins can be replaced by + * custom ones declared in hook_context_plugins(). + * + * @param $registry + * The registry, passed by reference. + */ +function hook_context_registry_alter(&$registry) { + if (isset($registry['reactions']['baz'])) { + $registry['reactions']['baz']['plugin'] = 'custom_context_reaction_baz'; + } +} + +/** + * Alter/add a condition to a node-related event. + * + * Allows modules to add one or more context condition plugin executions to a + * node view, form, etc. + * + * @param $node + * The node object. + * @param $op + * The node-related operation: 'node', 'form', 'comment'. + */ +function hook_context_node_condition_alter(&$node, $op) { + if ($plugin = context_get_plugin('condition', 'bar')) { + $plugin->execute($node, $op); + } +} + +/** + * Alter a context directly after it has been loaded. Allows modules to alter + * a context object's reactions. While you may alter conditions, this will + * generally have no effect as conditions are cached for performance and + * contexts are loaded after conditions are checked, not before. + * + * @param &$context + * The context object by reference. + */ +function hook_context_load_alter(&$context) { + if ($context->name === 'foo' && isset($context->reactions['block'])) { + $context->reactions['block']['blocks']['locale-0'] = array( + 'module' => 'locale', + 'delta' => '0', + 'region' => 'header', + 'weight' => '2', + ); + } +} + +/** + * Allows for finer grained access mechanisms to using the json + * rendering capabilities of the block reaction when a user isn't + * granted the administer contexts or context ajax block access + * permission + * @param $block_id + * ID of block in module-delta format + */ +function hook_context_allow_ajax_block_access($block_id) { +} diff --git a/context.core.inc b/context.core.inc new file mode 100644 index 0000000..41ed93a --- /dev/null +++ b/context.core.inc @@ -0,0 +1,385 @@ +' . check_plain($output) . ''; + } +} + +/** + * Implementation of hook_theme(). + */ +function context_theme() { + $items = array(); + if (!module_exists('block')) { + $items['block'] = array( + 'render element' => 'elements', + 'template' => 'block', + 'path' => drupal_get_path('module', 'block'), + 'file' => 'block.module', + 'template' => 'block', + ); + } + $items['context_block_form'] = array( + 'render element' => 'form', + 'path' => drupal_get_path('module', 'context') . '/theme', + 'file' => 'context_reaction_block.theme.inc', + ); + $items['context_block_regions_form'] = array( + 'render element' => 'form', + 'path' => drupal_get_path('module', 'context') . '/theme', + 'file' => 'context_reaction_block.theme.inc', + ); + $items['context_block_editor'] = array( + 'render element' => 'form', + 'path' => drupal_get_path('module', 'context') . '/theme', + 'file' => 'context_reaction_block.theme.inc', + ); + $items['context_block_browser'] = array( + 'variables' => array('blocks' => array(), 'context' => array()), + 'path' => drupal_get_path('module', 'context') . '/theme', + 'template' => 'context-block-browser', + 'file' => 'context_reaction_block.theme.inc', + ); + $items['context_block_browser_item'] = array( + 'variables' => array('block' => array()), + 'path' => drupal_get_path('module', 'context') . '/theme', + 'template' => 'context-block-browser-item', + 'file' => 'context_reaction_block.theme.inc', + ); + $items['context_block_script_placeholder'] = array( + 'variables' => array('text' => NULL), + 'path' => drupal_get_path('module', 'context') . '/theme', + 'file' => 'context_reaction_block.theme.inc', + ); + $items['context_block_edit_wrap'] = array( + 'render element' => 'element', + 'path' => drupal_get_path('module', 'context') . '/theme', + 'file' => 'context_reaction_block.theme.inc', + ); + return $items; +} + +/** + * Implementation of hook_theme_registry_alter(). + */ +function context_theme_registry_alter(&$theme_registry) { + // Push theme_page() through a context_preprocess to provide + // context-sensitive menus and variables. Ensure that + // context_preprocess_page() comes immediately after + // template_preprocess_page(). + $position = array_search('context_preprocess_page', $theme_registry['page']['preprocess functions']); + if ($position !== FALSE) { + unset($theme_registry['page']['preprocess functions'][$position]); + } + // Prevent conflict with i18n_menu. + if (module_exists('i18n_menu')) { + $position = array_search('i18n_menu_preprocess_page', $theme_registry['page']['preprocess functions']); + } + else { + $position = array_search('template_preprocess_page', $theme_registry['page']['preprocess functions']); + } + $position = $position ? $position + 1 : 2; + array_splice($theme_registry['page']['preprocess functions'], $position, 0, 'context_preprocess_page'); +} + +/** + * Implementation of hook_ctools_render_alter(). + * Used to detect the presence of a page manager node view or node form. + */ +function context_ctools_render_alter($info, $page, $data) { + extract($data); + if ($page && in_array($task['name'], array('node_view', 'node_edit'), TRUE)) { + foreach ($contexts as $ctools_context) { + if (in_array('node', $ctools_context->type) && !empty($ctools_context->data)) { + context_node_condition($ctools_context->data, $task['name'] === 'node_view' ? 'view' : 'form'); + break; + } + } + } +} + +/** + * Implementation of hook_entity_prepare_view(). + */ +function context_entity_prepare_view($prepare, $entity_type) { + if ($entity_type === 'taxonomy_term' && count($prepare) === 1) { + $term = reset($prepare); + $menu = menu_get_object('taxonomy_term', 2); + if ($menu && $term->tid == $menu->tid && $plugin = context_get_plugin('condition', 'taxonomy_term')) { + $plugin->execute($term, 'view'); + } + } +} + +/** + * Implementation of hook_node_view(). + */ +function context_node_view($node, $view_mode) { + $object = menu_get_object(); + if (isset($object->nid) && $object->nid === $node->nid) { + context_node_condition($node, 'view'); + } +} + +/** + * Implementation of hook_form_alter(). + */ +function context_form_alter(&$form, $form_state, $form_id) { + // If the form is an admin for, flag it so that we can force a rebuild if needed. + if (path_is_admin($_GET['q'])) { + $form['#submit'][] = 'context_admin_form_submit'; + } + // Trigger the condition in an after_build function to avoid being skipped + // when there are validation errors. + $form['#after_build'][] = 'context_form_alter_node_after_build'; +} + +/** + * Form #after_build callback for context_form_alter(). + */ +function context_form_alter_node_after_build($form, &$form_state) { + // Prevent this from firing on admin pages... damn form driven apis... + if (!empty($form['#node_edit_form']) && arg(0) != 'admin') { + context_node_condition($form['#node'], 'form'); + } + return $form; +} + +/** + * Clear out block info cache when an admin area form is submitted. + */ +function context_admin_form_submit(&$form, $form_state) { + if ($plugin = context_get_plugin('reaction', 'block')) { + $plugin->rebuild_needed(TRUE); + } +} + +/** + * Centralized node condition call function for the ever increasing number of + * ways to get at a node view / node form. + */ +function context_node_condition(&$node, $op) { + if ($plugin = context_get_plugin('condition', 'node')) { + $plugin->execute($node, $op); + } + if (module_exists('taxonomy')) { + if ($plugin = context_get_plugin('condition', 'node_taxonomy')) { + $plugin->execute($node, $op); + } + } + if (module_exists('book')) { + if ($plugin = context_get_plugin('condition', 'book')) { + $plugin->execute($node, $op); + } + if ($plugin = context_get_plugin('condition', 'bookroot')) { + $plugin->execute($node, $op); + } + } + // Allow other plugins to easily be triggered on node-related events. + drupal_alter('context_node_condition', $node, $op); +} + +/** + * Implementation of hook_form_alter() for system_modules_form. + */ +function context_form_system_modules_form_alter(&$form, $form_state) { + context_invalidate_cache(); +} + +/** + * Implementation of hook_form_alter() for user_profile_form. + */ +function context_form_user_profile_form_alter(&$form, $form_state) { + if ($plugin = context_get_plugin('condition', 'user_page')) { + $plugin->execute($form['#user'], 'form'); + } +} + +/** + * Implementation of hook_form_alter() for user_register_form. + */ +function context_form_user_register_form_alter(&$form, $form_state) { + if ($plugin = context_get_plugin('condition', 'user_page')) { + $plugin->execute($form['#user'], 'register'); + } +} + +/** + * Implementation of hook_form_alter() for comment_form. + */ +function context_form_comment_form_alter(&$form, $form_state) { + if ($nid = $form['nid']['#value']) { + $node = node_load($nid); + context_node_condition($node, 'comment'); + } +} + +/** + * Implementation of hook_views_pre_view(). + */ +function context_views_pre_view($view, $display) { + if ($plugin = context_get_plugin('condition', 'views')) { + $plugin->execute($view); + } + // Support Views overrides of specific entity paths. + if ($view->display_handler->has_path()) { + switch ($view->display_handler->get_option('path')) { + case 'taxonomy/term/%': + if (($term = taxonomy_term_load(arg(2))) && ($plugin = context_get_plugin('condition', 'taxonomy_term'))) { + $plugin->execute($term, 'view'); + } + break; + case 'node/%': + if ($node = node_load(arg(1))) { + context_node_condition($node, 'view'); + } + break; + case 'user/%': + if (($account = user_load(arg(1))) && ($plugin = context_get_plugin('condition', 'user_page'))) { + $plugin->execute($account, 'view'); + } + break; + } + } +} + +/** + * Implementation of hook_user(). + */ +function context_user_view($account, $view_mode) { + if ($view_mode === 'full' && $plugin = context_get_plugin('condition', 'user_page')) { + $plugin->execute($account, 'view'); + } +} + +/** + * Implements hook_page_build(). + */ +function context_page_build(&$page) { + module_invoke_all('context_page_condition'); + module_invoke_all('context_page_reaction'); + if ($plugin = context_get_plugin('reaction', 'block')) { + $plugin->execute($page); + } + + // See block_page_build. Clear static cache b/c in overlay form submissions + // hook_page_build can get called more than once per page load. + drupal_static_reset('context_reaction_block_list'); +} + +/** + * THEME FUNCTIONS & RELATED ========================================== + */ + +/** + * Generates an array of links (suitable for use with theme_links) + * to the node forms of types associated with current active contexts. + */ +function context_links($reset = FALSE) { + static $links; + if (!$links || $reset) { + $contexts = context_active_contexts(); + $active_types = array(); + $conditions = array('node', 'bookroot'); + foreach ($conditions as $condition) { + foreach ($contexts as $k => $v) { + if (!empty($v->conditions[$condition]['values'])) { + $active_types = array_merge($active_types, array_filter($v->conditions[$condition]['values'])); + } + } + } + + $links = array(); + if (!empty($active_types)) { + // Iterate over active contexts + foreach ($active_types as $type) { + $add_url = 'node/add/' . str_replace('_', '-', $type); + $item = menu_get_item($add_url); + if ($item && $item['access'] && strpos($_GET['q'], $add_url) !== 0) { + $links[$type] = array('title' => t('Add @type', array('@type' => node_type_get_name($type))), 'href' => $add_url); + } + } + } + drupal_alter('context_links', $links); + uasort($links, 'element_sort'); + } + return $links; +} + +/** + * Implementation of hook_context_page_condition(). + */ +function context_context_page_condition() { + if ($plugin = context_get_plugin('condition', 'menu')) { + $plugin->execute(); + } + if ($plugin = context_get_plugin('condition', 'default')) { + $plugin->execute(1); + } + if ($plugin = context_get_plugin('condition', 'context')) { + $plugin->execute(); + } + if ($plugin = context_get_plugin('condition', 'context_all')) { + $plugin->execute(); + } +} + +/** + * Implementation of hook_context_page_reaction(). + */ +function context_context_page_reaction() { + if ($plugin = context_get_plugin('reaction', 'css_injector')) { + $plugin->execute(); + } + if ($plugin = context_get_plugin('reaction', 'debug')) { + $plugin->execute(); + } +} + +/** + * Implementation of hook_preprocess_page(). + */ +function context_preprocess_page(&$vars) { + if ($plugin = context_get_plugin('reaction', 'theme')) { + $plugin->execute($vars); + } + if ($plugin = context_get_plugin('reaction', 'template_suggestions')) { + $plugin->execute($vars); + } + /* + if ($context_links = context_links()) { + $vars['context_links'] = theme('links', $context_links); + } + else { + $vars['context_links'] = ''; + } + */ +} + +/** +* Implementation of hook_delivery_callback_alter(). +* Based on menu_position's and menu_trail_by_path's implementations. +*/ +function context_page_delivery_callback_alter() { + if ($plugin = context_get_plugin('reaction', 'menu')) { + $plugin->execute(); + } + if ($plugin = context_get_plugin('reaction', 'breadcrumb')) { + $plugin->execute(); + } +} + +/** + * Implementation of hook_preprocess_html(). + */ +function context_preprocess_html(&$vars) { + if ($plugin = context_get_plugin('reaction', 'theme_html')) { + $plugin->execute($vars); + } +} diff --git a/context.info b/context.info new file mode 100644 index 0000000..e0b6ca3 --- /dev/null +++ b/context.info @@ -0,0 +1,9 @@ +name = "Context" +dependencies[] = "ctools" +description = "Provide modules with a cache that lasts for a single page request." +package = "Context" +core = "7.x" + +files[] = tests/context.test +files[] = tests/context.conditions.test +files[] = tests/context.reactions.test diff --git a/context.install b/context.install new file mode 100644 index 0000000..0a5a145 --- /dev/null +++ b/context.install @@ -0,0 +1,112 @@ + 'Storage for normal (user-defined) contexts.', + 'export' => array( + 'key' => 'name', + 'identifier' => 'context', + 'default hook' => 'context_default_contexts', // Function hook name. + 'status' => 'context_status', + 'api' => array( + 'owner' => 'context', + 'api' => 'context', // Base name for api include files. + 'minimum_version' => 3, + 'current_version' => 3, + ), + 'export callback' => 'context_export', + ), + 'fields' => array( + 'name' => array( + 'description' => 'The primary identifier for a context.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'description' => array( + 'description' => 'Description for this context.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'tag' => array( + 'description' => 'Tag for this context.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'conditions' => array( + 'description' => 'Serialized storage of all context condition settings.', + 'type' => 'text', + 'serialize' => TRUE, + ), + 'reactions' => array( + 'description' => 'Serialized storage of all context reaction settings.', + 'type' => 'text', + 'serialize' => TRUE, + ), + 'condition_mode' => array( + 'description' => 'Condition mode for this context.', + 'type' => 'int', + 'default' => 0, + ), + ), + 'primary key' => array('name'), + ); + return $schema; +} + +/** + * Update 7000: Handle adjustments to split of theme reaction to support D7 preprocess split between _page and _html + */ + +function context_update_7000() { + drupal_load('module', 'ctools'); + drupal_load('module', 'context'); + $updated = array(); + $contexts = context_load(NULL, TRUE); + foreach ($contexts as $c) { + // if the old data is in the old reaction and the new reaction hasn't been saved, migrate the old data to the new reaction + if (isset($c->reactions['theme']) && + isset($c->reactions['theme']['class']) && + !empty($c->reactions['theme']['class']) && + !isset($c->reactions['theme_html']) + ) { + $c->reactions['theme_html']['class'] = $c->reactions['theme']['class']; + context_save($c); + $updated[] = $c->name; + } + } + if (empty($updated)) { + $ret = t('No contexts requiring migration detected'); + } + else { + $ret = t('The following contexts had theme reaction data migrated: @names', array('@names' => join(', ', $updated))); + } + return $ret; +} diff --git a/context.module b/context.module new file mode 100644 index 0000000..96808dd --- /dev/null +++ b/context.module @@ -0,0 +1,569 @@ + array( + 'cache' => TRUE, + 'use hooks' => TRUE, + 'classes' => array('handler'), + ), + ); +} + +/** + * Implementation of hook_context_plugins(). + * + * This is a ctools plugins hook. + */ +function context_context_plugins() { + module_load_include('inc', 'context', 'context.plugins'); + return _context_context_plugins(); +} + + +/** + * Implementation of hook_context_registry(). + */ +function context_context_registry() { + module_load_include('inc', 'context', 'context.plugins'); + return _context_context_registry(); +} + +/** + * Implementation of hook_init(). + */ +function context_init() { + if ($plugin = context_get_plugin('condition', 'sitewide')) { + $plugin->execute(1); + } + if ($plugin = context_get_plugin('condition', 'path')) { + $plugin->execute(); + } + if ($plugin = context_get_plugin('condition', 'query_string')) { + $plugin->execute(); + } + if ($plugin = context_get_plugin('condition', 'language')) { + global $language; + $plugin->execute($language->language); + } + if ($plugin = context_get_plugin('condition', 'user')) { + global $user; + $plugin->execute($user); + } +} + +/** + * Implementation of hook_preprocess_menu_link(). + * + * This allows menus that are not primary/secondary menus to get + * the "active" class assigned to them. This assumes they are using + * theme('menu_link') for the menu rendering to html. + */ +function context_preprocess_menu_link(&$variables) { + if($contexts = context_active_contexts()){ + foreach($contexts as $context){ + if((isset($context->reactions['menu']))){ + if ($variables['element']['#href'] == $context->reactions['menu']) { + $variables['element']['#localized_options']['attributes']['class'][] = "active"; + } + } + } + } +} + +/** + * Load & crud functions ============================================== + */ + +/** + * Context loader. + * + * @param $name + * The name for this context object. + * + * @return + * Returns a fully-loaded context definition. + */ +function context_load($name = NULL, $reset = FALSE) { + ctools_include('export'); + static $contexts; + static $altered; + if (!isset($contexts) || $reset) { + $contexts = $altered = array(); + if (!$reset && $contexts = context_cache_get('context')) { + // Nothing here. + } + else { + if ($reset) { + ctools_export_load_object_reset('context'); + } + $contexts = ctools_export_load_object('context', 'all'); + context_cache_set('context', $contexts); + } + } + if (isset($name)) { + // Allow other modules to alter the value just before it's returned. + if (isset($contexts[$name]) && !isset($altered[$name])) { + $altered[$name] = TRUE; + drupal_alter('context_load', $contexts[$name]); + } + return isset($contexts[$name]) ? $contexts[$name] : FALSE; + } + return $contexts; +} + +/** + * Inserts or updates a context object into the database. + * @TODO: should probably return the new cid on success -- make sure + * this doesn't break any checks elsewhere. + * + * @param $context + * The context object to be inserted. + * + * @return + * Returns true on success, false on failure. + */ +function context_save($context) { + $existing = context_load($context->name, TRUE); + if ($existing && ($existing->export_type & EXPORT_IN_DATABASE)) { + drupal_write_record('context', $context, 'name'); + } + else { + drupal_write_record('context', $context); + } + context_load(NULL, TRUE); + context_invalidate_cache(); + return TRUE; +} + +/** + * Deletes an existing context. + * + * @param $context + * The context object to be deleted. + * + * @return + * Returns true on success, false on failure. + */ +function context_delete($context) { + if (isset($context->name) && ($context->export_type & EXPORT_IN_DATABASE)) { + db_query("DELETE FROM {context} WHERE name = :name", array(':name' => $context->name)); + context_invalidate_cache(); + return TRUE; + } + return FALSE; +} + +/** + * Exports the specified context. + */ +function context_export($context, $indent = '') { + $output = ctools_export_object('context', $context, $indent); + $translatables = array(); + foreach (array('description', 'tag') as $key) { + if (!empty($context->{$key})) { + $translatables[] = $context->{$key}; + } + } + $translatables = array_filter(array_unique($translatables)); + if (!empty($translatables)) { + $output .= "\n"; + $output .= "{$indent}// Translatables\n"; + $output .= "{$indent}// Included for use with string extractors like potx.\n"; + sort($translatables); + foreach ($translatables as $string) { + $output .= "{$indent}t(" . ctools_var_export($string) . ");\n"; + } + } + return $output; +} + +/** + * API FUNCTIONS ====================================================== + */ + +/** + * CTools list callback for bulk export. + */ +function context_context_list() { + $contexts = context_load(NULL, TRUE); + $list = array(); + foreach ($contexts as $context) { + $list[$context->name] = $context->name; + } + return $list; +} + +/** + * Wrapper around cache_get() to make it easier for context to pull different + * datastores from a single cache row. + */ +function context_cache_get($key, $reset = FALSE) { + static $cache; + if (!isset($cache) || $reset) { + $cache = cache_get('context', 'cache'); + $cache = $cache ? $cache->data : array(); + } + return !empty($cache[$key]) ? $cache[$key] : FALSE; +} + +/** + * Wrapper around cache_set() to make it easier for context to write different + * datastores to a single cache row. + */ +function context_cache_set($key, $value) { + $cache = cache_get('context', 'cache'); + $cache = $cache ? $cache->data : array(); + $cache[$key] = $value; + cache_set('context', $cache); +} + +/** + * Wrapper around context_load() that only returns enabled contexts. + */ +function context_enabled_contexts($reset = FALSE) { + $enabled = array(); + foreach (context_load(NULL, $reset) as $context) { + if (empty($context->disabled)) { + $enabled[$context->name] = $context; + } + } + return $enabled; +} + +/** + * Queue or activate contexts that have met the specified condition. + * + * @param $context + * The context object to queue or activate. + * @param $condition + * String. Name for the condition that has been met. + * @param $reset + * Reset flag for the queue static cache. + */ +function context_condition_met($context, $condition, $reset = FALSE) { + static $queue; + if (!isset($queue) || $reset) { + $queue = array(); + } + if (!context_isset('context', $context->name)) { + // Context is using AND mode. Queue it. + if (isset($context->condition_mode) && $context->condition_mode == CONTEXT_CONDITION_MODE_AND) { + $queue[$context->name][$condition] = $condition; + + // If all conditions have been met. set the context. + if (!array_diff(array_keys($context->conditions), $queue[$context->name])) { + context_set('context', $context->name, $context); + } + } + // Context is using OR mode. Set it. + else { + context_set('context', $context->name, $context); + } + } +} + +/** + * Loads any active contexts with associated reactions. This should be run + * at a late stage of the page load to ensure that relevant contexts have been set. + */ +function context_active_contexts() { + $contexts = context_get('context'); + return !empty($contexts) && is_array($contexts) ? $contexts : array(); +} + +/** + * Loads an associative array of conditions => context identifiers to allow + * contexts to be set by different conditions. + */ +function context_condition_map($reset = FALSE) { + static $condition_map; + if (!isset($condition_map) || $reset) { + if (!$reset && $cache = context_cache_get('condition_map')) { + $condition_map = $cache; + } + else { + $condition_map = array(); + foreach (array_keys(context_conditions()) as $condition) { + if ($plugin = context_get_plugin('condition', $condition)) { + foreach (context_enabled_contexts() as $context) { + $values = $plugin->fetch_from_context($context, 'values'); + foreach ($values as $value) { + if (!isset($condition_map[$condition][$value])) { + $condition_map[$condition][$value] = array(); + } + $condition_map[$condition][$value][] = $context->name; + } + } + } + } + context_cache_set('condition_map', $condition_map); + } + } + return $condition_map; +} + +/** + * Invalidates all context caches(). + * @TODO: Update to use a CTools API function for clearing plugin caches + * when/if it becomes available. + */ +function context_invalidate_cache() { + cache_clear_all('context', 'cache', TRUE); + cache_clear_all('plugins:context', 'cache', TRUE); +} + +/** + * Implementation of hook_flush_caches(). + */ +function context_flush_caches() { + context_invalidate_cache(); +} + +/** + * Recursive helper function to determine whether an array and its + * children are entirely empty. + */ +function context_empty($element) { + $empty = TRUE; + if (is_array($element)) { + foreach ($element as $child) { + $empty = $empty && context_empty($child); + } + } + else { + $empty = $empty && !isset($element); + } + return $empty; +} + +/** + * Get a plugin handler. + */ +function context_get_plugin($type = 'condition', $key, $reset = FALSE) { + static $cache = array(); + if (!isset($cache[$type][$key]) || $reset) { + switch ($type) { + case 'condition': + $registry = context_conditions(); + break; + case 'reaction': + $registry = context_reactions(); + break; + } + if (isset($registry[$key], $registry[$key]['plugin'])) { + ctools_include('plugins'); + $info = $registry[$key]; + $plugins = ctools_get_plugins('context', 'plugins'); + if (isset($plugins[$info['plugin']]) && $class = ctools_plugin_get_class($plugins[$info['plugin']], 'handler')) { + // Check that class exists until CTools & registry issues are resolved. + if (class_exists($class)) { + $cache[$type][$key] = new $class($key, $info); + } + } + } + } + return isset($cache[$type][$key]) ? $cache[$type][$key] : FALSE; +} + +/** + * Get all context conditions. + */ +function context_conditions($reset = FALSE) { + return _context_registry('conditions', $reset); +} + +/** + * Get all context reactions. + */ +function context_reactions($reset = FALSE) { + return _context_registry('reactions', $reset); +} + +/** + * Retrieves & caches the context registry. + */ +function _context_registry($key = NULL, $reset = FALSE) { + static $registry; + if (!isset($registry) || $reset) { + if (!$reset && $cache = context_cache_get('registry')) { + $registry = $cache; + } + else { + $registry = module_invoke_all('context_registry'); + drupal_alter('context_registry', $registry); + context_cache_set('registry', $registry); + } + } + if (isset($key)) { + return isset($registry[$key]) ? $registry[$key] : array(); + } + return $registry; +} + +/** + * hook_block_view_alter - if the context editor block is on this page, + * ensure that all blocks have some content so that empty blocks are + * not dropped + */ +function context_block_view_alter(&$data, $block) { + if (context_isset('context_ui', 'context_ui_editor_present') && empty($data['content'])) { + $data['content']['#markup'] = "
" . t('This block appears empty when displayed on this page.') . "
"; + $data['context_block_hidden'] = TRUE; + } +} + +/** + * implement hook_page_alter() + * + * used for region context + */ +function context_page_alter(&$page) { + if ($plugin = context_get_plugin('reaction', 'region')) { + $plugin->execute($page); + } +} + +/** + * hook_block_view_alter - if the context editor block is on this page, + * ensure that all blocks have some content so that empty blocks are + * not dropped + */ +function context_preprocess_block(&$vars) { + if (isset($vars['block']->context_block_hidden)) { + $vars['classes_array'][] = 'context-block-hidden'; + $vars['classes_array'][] = 'context-block-empty'; + } +} diff --git a/context.plugins.inc b/context.plugins.inc new file mode 100644 index 0000000..a0849bb --- /dev/null +++ b/context.plugins.inc @@ -0,0 +1,392 @@ + array( + 'title' => t('Context (any)'), + 'description' => t('Set this context on the basis of other active contexts. Put each context on a separate line. The condition will pass if any of the contexts are active. You can use the * character (asterisk) as a wildcard and the ~ character (tilde) to prevent this context from activating if the listed context is active. Other contexts which use context conditions can not be used to exclude this context from activating.'), + 'plugin' => 'context_condition_context', + ), + 'context_all' => array( + 'title' => t('Context (all)'), + 'description' => t('Set this context on the basis of other active contexts. Put each context on a separate line. The condition will pass only if all of the contexts are active. You can use the * character (asterisk) as a wildcard and the ~ character (tilde) to prevent this context from activating if the listed context is active. Other contexts which use context conditions can not be used to exclude this context from activating.'), + 'plugin' => 'context_condition_context_all', + ), + 'node' => array( + 'title' => t('Node type'), + 'description' => t('Set this context when viewing a node page or using the add/edit form of one of these content types.'), + 'plugin' => 'context_condition_node', + ), + 'sitewide' => array( + 'title' => t('Sitewide context'), + 'description' => t('Should this context always be set? If true, this context will be active across your entire site.'), + 'plugin' => 'context_condition_sitewide', + ), + 'default' => array( + 'title' => t('Default context'), + 'description' => t('This context will be set if no other context is active except sitewide contexts.'), + 'plugin' => 'context_condition_default', + ), + 'path' => array( + 'title' => t('Path'), + 'description' => t('Set this context when any of the paths above match the page path. Put each path on a separate line. You can use the * character (asterisk) as a wildcard and the ~ character (tilde) to exclude one or more paths. Use <front> for the site front page.'), + 'plugin' => 'context_condition_path', + ), + 'query_string' => array( + 'title' => t('Query String'), + 'description' => t('Set this context when any of the query strings above match the page query string. Put each query string on a separate line. You can use the "*" character as a wildcard and ~ to exclude one or more query strings.'), + 'plugin' => 'context_condition_query_string', + ), + 'user' => array( + 'title' => t('User role'), + 'description' => t('Set this context when the current user has one of the selected role(s).'), + 'plugin' => 'context_condition_user', + ), + 'user_page' => array( + 'title' => t('User page'), + 'description' => t('Set this context when viewing a user page.'), + 'plugin' => 'context_condition_user_page', + ), + ); + if (module_exists('menu')) { + $registry['conditions']['menu'] = array( + 'title' => t('Menu'), + 'description' => t('Set this context when any of the selected menu items belong to the current active menu trail.'), + 'plugin' => 'context_condition_menu', + ); + } + if (module_exists('views')) { + $registry['conditions']['views'] = array( + 'title' => t('Views'), + 'description' => t('Set this context when displaying the page of one of these views.'), + 'plugin' => 'context_condition_views', + ); + } + if (module_exists('book')) { + $registry['conditions']['book'] = array( + 'title' => t('Book'), + 'description' => t('Set this context when a node in the selected book is viewed.'), + 'plugin' => 'context_condition_book', + ); + $registry['conditions']['bookroot'] = array( + 'title' => t('Book root'), + 'description' => t('Set this context when viewing a node whose root book is of the selected type.'), + 'plugin' => 'context_condition_bookroot', + ); + } + if (module_exists('locale')) { + $registry['conditions']['language'] = array( + 'title' => t('Language'), + 'description' => t('Set this context when viewing the site in the selected language.'), + 'plugin' => 'context_condition_language', + ); + } + if (module_exists('taxonomy')) { + $registry['conditions']['node_taxonomy'] = array( + 'title' => t('Taxonomy'), + 'description' => t('Set this context when viewing a node with the selected taxonomy terms.'), + 'plugin' => 'context_condition_node_taxonomy', + ); + $registry['conditions']['taxonomy_term'] = array( + 'title' => t('Taxonomy term'), + 'description' => t('Set this context when viewing a taxonomy term page.'), + 'plugin' => 'context_condition_taxonomy_term', + ); + } + $registry['reactions'] = array( + 'block' => array( + 'title' => t('Blocks'), + 'description' => t('Control block visibility using context.'), + 'plugin' => 'context_reaction_block', + ), + 'region' => array( + 'title' => t('Regions'), + 'description' => t('Control Region visiblity using context.'), + 'plugin' => 'context_reaction_region', + ), + 'breadcrumb' => array( + 'title' => t('Breadcrumb'), + 'description' => t('Set the breadcrumb trail to the selected menu item.'), + 'plugin' => 'context_reaction_breadcrumb', + ), + 'template_suggestions' => array( + 'title' => t('Template suggestions'), + 'description' => t('Add template suggestions using context.'), + 'plugin' => 'context_reaction_template_suggestions', + ), + 'theme' => array( + 'title' => t('Theme Page'), + 'description' => t('Control page theme variables using context.'), + 'plugin' => 'context_reaction_theme', + ), + 'theme_html' => array( + 'title' => t('Theme HTML'), + 'description' => t('Control HTML theme variables using context.'), + 'plugin' => 'context_reaction_theme_html', + ), + 'debug' => array( + 'title' => t('Debug'), + 'description' => t('Debug output reaction for SimpleTest.'), + 'plugin' => 'context_reaction_debug', + ), + ); + if (module_exists('menu')) { + $registry['reactions']['menu'] = array( + 'title' => t('Menu'), + 'description' => t('Control menu active class using context.'), + 'plugin' => 'context_reaction_menu', + ); + } + if (module_exists('css_injector')) { + $registry['reactions']['css_injector'] = array( + 'title' => t('CSS Injector'), + 'description' => t('Inject the selected css when this context is set.'), + 'plugin' => 'context_reaction_css_injector', + ); + } + return $registry; +} + +/** + * Context plugins. + */ +function _context_context_plugins() { + $plugins = array(); + + /** + * Conditions. + */ + $plugins['context_condition'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition.inc', + 'class' => 'context_condition', + ), + ); + $plugins['context_condition_context'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_context.inc', + 'class' => 'context_condition_context', + 'parent' => 'context_condition_path', + ), + ); + $plugins['context_condition_context_all'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_context_all.inc', + 'class' => 'context_condition_context_all', + 'parent' => 'context_condition_path', + ), + ); + $plugins['context_condition_node'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_node.inc', + 'class' => 'context_condition_node', + 'parent' => 'context_condition', + ), + ); + $plugins['context_condition_sitewide'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_sitewide.inc', + 'class' => 'context_condition_sitewide', + 'parent' => 'context_condition', + ), + ); + $plugins['context_condition_default'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_default.inc', + 'class' => 'context_condition_default', + 'parent' => 'context_condition', + ), + ); + $plugins['context_condition_path'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_path.inc', + 'class' => 'context_condition_path', + 'parent' => 'context_condition', + ), + ); + $plugins['context_condition_query_string'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') .'/plugins', + 'file' => 'context_condition_query_string.inc', + 'class' => 'context_condition_query_string', + 'parent' => 'context_condition_path', + ), + ); + $plugins['context_condition_user'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_user.inc', + 'class' => 'context_condition_user', + 'parent' => 'context_condition', + ), + ); + $plugins['context_condition_user_page'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_user_page.inc', + 'class' => 'context_condition_user_page', + 'parent' => 'context_condition', + ), + ); + $plugins['context_condition_menu'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_menu.inc', + 'class' => 'context_condition_menu', + 'parent' => 'context_condition', + ), + ); + if (module_exists('taxonomy')) { + $plugins['context_condition_node_taxonomy'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_node_taxonomy.inc', + 'class' => 'context_condition_node_taxonomy', + 'parent' => 'context_condition_node', + ), + ); + $plugins['context_condition_taxonomy_term'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_taxonomy_term.inc', + 'class' => 'context_condition_taxonomy_term', + 'parent' => 'context_condition', + ), + ); + } + if (module_exists('locale')) { + $plugins['context_condition_language'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_language.inc', + 'class' => 'context_condition_language', + 'parent' => 'context_condition', + ), + ); + } + if (module_exists('book')) { + $plugins['context_condition_book'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_book.inc', + 'class' => 'context_condition_book', + 'parent' => 'context_condition', + ), + ); + $plugins['context_condition_bookroot'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_bookroot.inc', + 'class' => 'context_condition_bookroot', + 'parent' => 'context_condition_node', + ), + ); + } + if (module_exists('views')) { + $plugins['context_condition_views'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_condition_views.inc', + 'class' => 'context_condition_views', + 'parent' => 'context_condition', + ), + ); + } + + /** + * Reactions. + */ + $plugins['context_reaction'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction.inc', + 'class' => 'context_reaction', + ), + ); + $plugins['context_reaction_block'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_block.inc', + 'class' => 'context_reaction_block', + 'parent' => 'context_reaction', + ), + ); + $plugins['context_reaction_region'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_region.inc', + 'class' => 'context_reaction_region', + 'parent' => 'context_reaction', + ), + ); + $plugins['context_reaction_breadcrumb'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_breadcrumb.inc', + 'class' => 'context_reaction_breadcrumb', + 'parent' => 'context_reaction_menu', + ), + ); + $plugins['context_reaction_template_suggestions'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_template_suggestions.inc', + 'class' => 'context_reaction_template_suggestions', + 'parent' => 'context_reaction', + ), + ); + $plugins['context_reaction_menu'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_menu.inc', + 'class' => 'context_reaction_menu', + 'parent' => 'context_reaction', + ), + ); + $plugins['context_reaction_theme'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_theme.inc', + 'class' => 'context_reaction_theme', + 'parent' => 'context_reaction', + ), + ); + $plugins['context_reaction_theme_html'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_theme_html.inc', + 'class' => 'context_reaction_theme_html', + 'parent' => 'context_reaction_theme', + ), + ); + $plugins['context_reaction_debug'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_debug.inc', + 'class' => 'context_reaction_debug', + 'parent' => 'context_reaction', + ), + ); + if (module_exists('css_injector')) { + $plugins['context_reaction_css_injector'] = array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context') . '/plugins', + 'file' => 'context_reaction_css_injector.inc', + 'class' => 'context_reaction_css_injector', + 'parent' => 'context_reaction', + ), + ); + } + return $plugins; +} diff --git a/context_layouts/README.txt b/context_layouts/README.txt new file mode 100644 index 0000000..a0b3a04 --- /dev/null +++ b/context_layouts/README.txt @@ -0,0 +1,85 @@ + +Context layouts +--------------- +Context layouts provides a formalized way for themes to declare and switch +between page templates using Context. It is a continuation of an old Drupal +themer's trick to switch to something besides the standard `page.tpl.php` file +for a variety of special-case pages like the site frontpage, login page, admin +section, etc. + + +Requirements +------------ +In order to use context layouts, your site must meet a few conditions: + +- Context and Context layouts modules are enabled (`admin/modules`). +- You are using a theme which provides and has declared multiple layouts. (See + "Example themes" for themes you can try.) + + +Basic usage +----------- +Once you have layouts enabled, you can have a context trigger the usage of a +particular layout in either the admin interface (`admin/structure/context`) or +inline context editor. Different layouts may have fewer or greater regions than +the default page template, so adjust your blocks accordingly. + + +Supporting context layouts in your theme +---------------------------------------- +You can add layouts support to your theme by declaring additional layouts in +your theme's info file. Here is an example: + +`example.info` + + name = "Example" + description = "Example theme" + core = "6.x" + engine = "phptemplate" + + regions[left] = "Left sidebar" + regions[right] = "Right sidebar" + regions[content] = "Content" + regions[footer] = "Footer" + + ; Layout: Default + layouts[default][name] = "Default" + layouts[default][description] = "Simple two column page." + layouts[default][template] = "page" + layouts[default][regions][] = "content" + layouts[default][regions][] = "right" + + ; Layout: Columns + layouts[columns][name] = "3 columns" + layouts[columns][description] = "Three column page." + layouts[columns][stylesheet] = "layout-columns.css" + layouts[columns][template] = "layout-columns" + layouts[columns][regions][] = "left" + layouts[columns][regions][] = "content" + layouts[columns][regions][] = "right" + layouts[columns][regions][] = "footer" + +Each layout is declared under `layouts` with the key as the identifier that will +be used by context for this layout. You may use any reasonable machine name for +each layout, but note that `default` is special -- it will be the default layout +for your theme if no other layout is specified. + +The following keys can be declared for each layout: + +- `name`: The human readable name for this layout, shown in the admin UI. +- `description`: A short description of your layout, same as above. +- `stylesheet`: A stylesheet to be included with the layout. Optional. +- `template`: The name of the template file for this layout, without the + `.tpl.php` extension. +- `region`: An array of regions supported by this layout. Note that any of the + regions listed here **must also be declared** in the standard theme `regions` + array. + + +Example themes +-------------- +- Cube, a subtheme included with [Rubik][1] provides a variety of layouts. +- [Ginkgo][2] the default theme included with Open Atrium. + +[1]: http://github.com/developmentseed/rubik/downloads +[2]: http://github.com/developmentseed/ginkgo/downloads diff --git a/context_layouts/context_layouts.info b/context_layouts/context_layouts.info new file mode 100644 index 0000000..0f37da6 --- /dev/null +++ b/context_layouts/context_layouts.info @@ -0,0 +1,7 @@ +name = Context layouts +description = Allow theme layer to provide multiple region layouts and integrate with context. +dependencies[] = context +package = Context +core = 7.x + +files[] = plugins/context_layouts_reaction_block.inc diff --git a/context_layouts/context_layouts.module b/context_layouts/context_layouts.module new file mode 100644 index 0000000..06cdff3 --- /dev/null +++ b/context_layouts/context_layouts.module @@ -0,0 +1,132 @@ +'. check_plain($output) .''; + } +} + +/** + * Implementation of hook_context_plugins(). + * This is a ctools plugins hook. + */ +function context_layouts_context_plugins() { + return array( + 'context_layouts_reaction_block' => array( + 'handler' => array( + 'path' => drupal_get_path('module', 'context_layouts') .'/plugins', + 'file' => 'context_layouts_reaction_block.inc', + 'class' => 'context_layouts_reaction_block', + 'parent' => 'context_reaction_block', + ), + ), + ); +} + +/** + * Implementation of hook_context_registry_alter(). + */ +function context_layouts_context_registry_alter(&$registry) { + if (isset($registry['reactions']['block'])) { + $registry['reactions']['block']['plugin'] = 'context_layouts_reaction_block'; + } +} + +/** + * Implementation of hook_theme(). + * Declares each theme's layouts as a page template suggestion. + */ +function context_layouts_theme() { + $info = array(); + foreach (list_themes() as $theme) { + if (!empty($theme->status) && $layouts = context_layouts_get_layouts($theme->name)) { + foreach ($layouts as $layout) { + if (!empty($layout['template'])) { + $info["page__context_layouts_{$theme->name}_{$layout['layout']}"] = array( + 'template' => $layout['template'], + 'path' => $layout['path'], + ); + } + } + } + } + return $info; +} + +/** + * Implementation of hook_context_page_reaction(). + */ +function context_layouts_context_page_reaction() { + $plugin = context_get_plugin('reaction', 'block'); + if ($plugin && method_exists($plugin, 'add_layout_stylesheet')) { + $plugin->add_layout_stylesheet(); + } +} + +/** + * Preprocessor for theme('page'). + */ +function context_layouts_preprocess_page(&$vars) { + $plugin = context_get_plugin('reaction', 'block'); + if ($plugin && method_exists($plugin, 'add_layout_template')) { + $plugin->add_layout_template($vars); + } +} + +/** + * Retrieve layouts for the specified theme. + */ +function context_layouts_get_layouts($theme = NULL, $reset = FALSE) { + static $layouts = array(); + $layouts = $reset ? array() : $layouts; + + global $theme_key; + $theme = isset($theme) ? $theme : $theme_key; + + if (!isset($layouts[$theme])) { + $info = system_get_info('theme', $theme); + $themes = array(); + + // Find all our ancestor themes that use layouts. + if (isset($info['base theme'])) { + while (!empty($info['base theme'])) { + $base_theme = $info['base theme']; + $info = system_get_info('theme', $base_theme); + $themes[$base_theme] = $info; + } + } + + // Assemble in inheritance order and add the theme on. + $themes = array_reverse($themes); + $themes[$theme] = system_get_info('theme', $theme); + + // Merge layout info into a single array. + foreach ($themes as $key => $info) { + $path = drupal_get_path('theme', $key); + if (!empty($info['layouts'])) { + foreach ($info['layouts'] as $layout => $layout_info) { + $layout_info['layout'] = str_replace('-', '_', $layout); + $layout_info['theme'] = $key; + $layout_info['path'] = $path; + $layouts[$theme][$layout] = $layout_info; + } + } + } + } + return isset($layouts[$theme]) ? $layouts[$theme] : FALSE; +} + +/** + * Get the active layout for the current page. + */ +function context_layouts_get_active_layout($info = TRUE) { + $plugin = context_get_plugin('reaction', 'block'); + if ($plugin && method_exists($plugin, 'get_active_layout')) { + return $plugin->get_active_layout($info); + } +} diff --git a/context_layouts/plugins/context_layouts_reaction_block.css b/context_layouts/plugins/context_layouts_reaction_block.css new file mode 100644 index 0000000..2abbcb6 --- /dev/null +++ b/context_layouts/plugins/context_layouts_reaction_block.css @@ -0,0 +1,8 @@ +#admin-toolbar div.context-editor-block-layouts { + padding:0px 0px 9px; + border-bottom:1px solid #333; + margin:0px 0px 10px; + } + +#admin-toolbar div.context-editor-block-layouts div.form-item { display:inline; } +#admin-toolbar div.context-editor-block-layouts select { width:50%; } diff --git a/context_layouts/plugins/context_layouts_reaction_block.inc b/context_layouts/plugins/context_layouts_reaction_block.inc new file mode 100644 index 0000000..51bc928 --- /dev/null +++ b/context_layouts/plugins/context_layouts_reaction_block.inc @@ -0,0 +1,205 @@ +get_active_layout(); + if ($layout && isset($layout['regions']) && is_array($layout['regions'])) { + return in_array($region, $layout['regions'], TRUE) && parent::is_enabled_region($region); + } + return parent::is_enabled_region($region); + } + + /** + * Retrieve the first layout specified found by any active contexts. + */ + function get_active_layout($info = TRUE) { + $contexts = $this->get_contexts(); + $layouts = context_layouts_get_layouts(); + if (!empty($contexts) && !empty($layouts)) { + foreach ($contexts as $context) { + $values = $this->fetch_from_context($context); + if (isset($values['layout']) && isset($layouts[$values['layout']])) { + return $info ? $layouts[$values['layout']] : $values['layout']; + } + } + } + // Fallback to default layout if provided. + if (isset($layouts['default'])) { + return $info ? $layouts['default'] : 'default'; + } + return FALSE; + } + + /** + * Add the layout template to page vars. + */ + function add_layout_template(&$vars) { + if ($layout = $this->get_active_layout()) { + if (!empty($layout['template'])) { + global $theme; + $vars['theme_hook_suggestion'] = "page__context_layouts_{$theme}_{$layout['layout']}"; + } + } + } + + /** + * Add the layout stylesheet to the CSS. + */ + function add_layout_stylesheet() { + if ($layout = $this->get_active_layout()) { + if (!empty($layout['stylesheet'])) { + drupal_add_css(drupal_get_path('theme', $layout['theme']) . '/' . $layout['stylesheet']); + } + } + } + + /** + * Override of editor form. + */ + function editor_form($context) { + drupal_add_css(drupal_get_path('module', 'context_layouts') . '/plugins/context_layouts_reaction_block.css'); + + $form = parent::editor_form($context); + + if ($layouts = $this->get_layout_options()) { + $options = $this->fetch_from_context($context); + $form['layout'] = array( + // #tree *must* be true for our values to be nested correctly. + '#tree' => TRUE, + '#prefix' => '
', + '#suffix' => '
', + '#weight' => -100, + 'layout' => array( + '#title' => t('Layout'), + '#options' => $layouts, + '#type' => 'select', + '#weight' => -100, + '#default_value' => isset($options['layout']) ? $options['layout'] : NULL, + '#required' => FALSE, + '#empty_value' => 0, + '#empty_option' => '- ' . t('Site default') . ' -', + ), + 'update' => array( + '#value' => t('Change layout'), + '#type' => 'submit', + ), + ); + } + return $form; + } + + /** + * Override of editor form submit. + */ + function editor_form_submit(&$context, $values) { + // Someone has changed the layout, assume that the block values are not actually usable here. + if (isset($context->reactions['block']['layout']) && $context->reactions['block']['layout'] != $values['layout']['layout']) { + $options = $context->reactions['block']; + } + else { + $options = parent::editor_form_submit($context, $values); + } + + if (!empty($values['layout']['layout'])) { + $options['layout'] = $values['layout']['layout']; + } + else { + unset($options['layout']); + } + return $options; + } + + /** + * Override of options form. + */ + function options_form($context) { + $form = parent::options_form($context); + $options = $this->fetch_from_context($context); + + // Only alter the options form if the theme provides layouts. + $theme_key = variable_get('theme_default', 'garland'); + $layouts = $this->get_layout_options(); + if (!empty($layouts)) { + $form['layout'] = array( + '#title' => t('Layout'), + '#description' => t('Choose one of the layouts provided by the default theme.'), + '#options' => $layouts, + '#type' => 'select', + '#weight' => -100, + '#default_value' => !empty($options['layout']) ? $options['layout'] : NULL, + '#attributes' => array('class' => array('context-blockform-layout')), + '#required' => FALSE, + '#empty_value' => 0, + '#empty_option' => '- ' . t('Site default') . ' -', + ); + + // Add js. + // @TODO: Move this to a theme function or somewhere that will get called even + // if the form is using a cached version of itself (e.g. when validate fails). + drupal_add_js(drupal_get_path('module', 'context_layouts') . '/plugins/context_layouts_reaction_block.js'); + drupal_add_js(array('contextLayouts' => array('layouts' => $this->get_layout_regions())), 'setting'); + } + return $form; + } + + /** + * Override of submit handler. + */ + function options_form_submit($values) { + $options = parent::options_form_submit($values); + + // Only alter the options form if the theme provides layouts. + $theme_key = variable_get('theme_default', 'garland'); + $layouts = context_layouts_get_layouts($theme_key); + + // Check that this is a valid layout. + if (!empty($values['layout']) && isset($layouts[$values['layout']])) { + $layout = $values['layout']; + $options['layout'] = $layout; + + // Remove blocks that don't belong to regions in this layout. + if (isset($layouts[$layout]['regions'])) { + foreach ($options['blocks'] as $bid => $block) { + if (!in_array($block['region'], $layouts[$layout]['regions'])) { + unset($options['blocks'][$bid]); + } + } + } + } + return $options; + } + + /** + * Get layout options for the given theme. + */ + protected function get_layout_options($theme_key = NULL) { + $theme_key = !isset($theme_key) ? variable_get('theme_default', 'garland') : $theme_key; + $layouts = context_layouts_get_layouts($theme_key); + $layout_options = array(); + if (!empty($layouts)) { + foreach ($layouts as $layout => $info) { + $layout_options[$layout] = isset($info['name']) ? $info['name'] : $layout_options; + } + } + return $layout_options; + } + + /** + * Get a layout to region map for the given theme. + */ + protected function get_layout_regions($theme_key = NULL) { + $theme_key = !isset($theme_key) ? variable_get('theme_default', 'garland') : $theme_key; + $layouts = context_layouts_get_layouts($theme_key); + if (!empty($layouts)) { + $layout_regions = array(); + foreach ($layouts as $layout => $info) { + $layout_regions[$layout] = is_array($info['regions']) ? $info['regions'] : array(); + } + } + return $layout_regions; + } +} diff --git a/context_layouts/plugins/context_layouts_reaction_block.js b/context_layouts/plugins/context_layouts_reaction_block.js new file mode 100644 index 0000000..2c75ebb --- /dev/null +++ b/context_layouts/plugins/context_layouts_reaction_block.js @@ -0,0 +1,27 @@ + +(function($) { + +Drupal.behaviors.contextLayoutsReactionBlock = {}; +Drupal.behaviors.contextLayoutsReactionBlock.attach = function(context) { + // ContextBlockForm: Init. + $('.context-blockform-layout:not(.contextLayoutsProcessed)').each(function() { + $(this).addClass('contextLayoutsProcessed'); + $(this).change(function() { + var layout = $(this).val(); + if (Drupal.settings.contextLayouts.layouts[layout]) { + $('#context-blockform td.blocks').find('table, div.label, div.tabledrag-toggle-weight-wrapper').hide(); + for (var key in Drupal.settings.contextLayouts.layouts[layout]) { + var region = Drupal.settings.contextLayouts.layouts[layout][key]; + $('.context-blockform-regionlabel-'+region).show().next('div.tabledrag-toggle-weight-wrapper').show(); + $('#context-blockform-region-'+region).show(); + } + if (Drupal.contextBlockForm) { + Drupal.contextBlockForm.setState(); + } + } + }); + $(this).change(); + }); +}; + +})(jQuery); \ No newline at end of file diff --git a/context_ui/README.txt b/context_ui/README.txt new file mode 100644 index 0000000..78fed16 --- /dev/null +++ b/context_ui/README.txt @@ -0,0 +1,73 @@ + +Context UI +---------- +Context UI provides an administrative interface for managing and editing +Contexts. It is not necessary for the proper functioning of contexts once they +are built and can be turned off on most production sites. + + +Requirements +------------ +- Context, Context UI modules enabled (`admin/modules`) + + +Basic usage +----------- +As a site administrator you can manage your site's contexts at +`admin/structure/context`. The main page will show you a list of the contexts +on the site and give you some options for managing each context. + +When editing or adding a new context, you will be presented with a form to +manage some basic information about the context and then alter its conditions +and reactions. + +- `name`: The name of your context. This is the main identifier for your context + and cannot be changed after you've created it. +- `description`: A description or human-readable name for your context. This is + displayed in the inline editor if available instead of the name. +- `tag`: A category for organizing contexts in the administrative context + listing. Optional. + +**Conditions** + +When certain conditions are true, your context will be made active. You can +customize the conditions that trigger the activation of your context. + +- **Condition mode**: you can choose to have your context triggered if **ANY** + conditions are met or only active when **ALL** conditions are met. +- **Adding/removing conditions**: you can add or remove to the conditions on + your context using the conditions dropdown. +- **Individual settings**: most conditions provide a simple form for selecting + individual settings for that condition. For example, the node type condition + allows you to choose which node types activate the context. + +**Reactions** + +Whenever a particular context is active, all of its reactions will be run. +Like conditions, reactions can be added or removed and have settings that can +be configured. + +- **Reaction Block Groupings**: You can influence what "group" a block appears + in when listing all blocks available to be added to a region. This is done + by specifying $block->context_group via hook_block_info. If no group is + specified it will default to the module name, but if a group is specified + it will be grouped under that group name. + + + +Using the inline editor +----------------------- +The inline editor allows you to manage the block reaction for active +contexts within the context of a page rather than through the admin +interface. This can also be helpful when managing block ordering among +multiple contexts. + +1. As an administrative user go to `admin/structure/context/settings`. +2. Check the 'Use Context Editor Dialog' block and save. You should also + check the show all regions box. +3. When viewing a page with one or more active contexts, you will see + the option to configure layout in the contextual links on all blocks + on the page. This will allow you to manage the blocks placed by the + block reaction for contexts. +4. You can use the context editor to adjust the conditions under which each + context is active and alter its reactions. diff --git a/context_ui/context_ui.css b/context_ui/context_ui.css new file mode 100644 index 0000000..a0b3058 --- /dev/null +++ b/context_ui/context_ui.css @@ -0,0 +1,182 @@ +/** + * Editor ============================================================= + */ +div.context-editor div.label { + float:left; + font-size: 14px; +} +div.context-editor div.links { float:right; } +div.context-editor div.bottom { + font-size: 12px; + font-style: italic; + font-weight:normal; + color: #999; +} + +div.context-editor div.context-editable { display:none; } +div.context-editor div.links a.done { display:none; } +div.context-editor li.context-editing a.edit { display:none; } +div.context-editor li.context-editing a.done { display:block; } + +/* Don't display form submission buttons initially or when editing */ +body.context-editing div.context-editor div.links { display:none; } +body.context-editing div.context-editor li.context-editing div.links { display:block; } + +div.context-editor div.buttons { display:none; } +form.edited div.context-editor div.buttons { display:block; } +body.context-editing form.edited div.context-editor div.buttons { display:none; } + +/** + * Styles for visual integration with admin. + */ +#admin-toolbar div.context-editor div.links a:hover { + background:#eee; + color:#000; + } + +#admin-toolbar div.context-editor div.links a { + float:left; + + padding:0px 10px; + margin-right:5px; + + background:#222; + font-size:11px; + -moz-border-radius:10px; + -webkit-border-radius:10px; + } + +div.context-editor div.context-editable { padding:5px 0px; } + +#admin-toolbar div.context-editor div.buttons { + padding:10px 0px; + text-align:center; + background:#111; + } + +/** + * Horizontal + */ +#admin-toolbar.horizontal form.edited div.context-editor div.buttons, +#admin-toolbar.horizontal div.context-editor div.item-list { + float:left; + clear:left; + width:30%; + } + +#admin-toolbar.horizontal div.context-editor div.contexts { + width:69.9%; + float:right; + } + +#admin-toolbar.horizontal div.context-editor div.context-editable { padding-left:20px } + +/** + * Admin listing page ================================================= + */ +table.context-admin td.tag { font-style:italic; } + +table.context-admin td.ctools-export-ui-name { + width:75%; + padding-left:20px; + } + +table.context-admin td.ctools-export-ui-operations { white-space:nowrap; } + +table.context-admin td.ctools-export-ui-storage { color:#999; } + +table.context-admin div.description { + padding-left:10px; + margin:0px; + } + +table.context-admin input.form-text { width:90%; } + +/** + * Context form ======================================================= + */ +.context-plugins { + position:relative; + margin:0px 0px 10px; +} + +.context-plugins .context-plugin-info { + padding:10px 10px 9px; + border-bottom:1px solid #e8e8e8; + } + +.context-plugins .context-plugin-info div.description { + margin:0px 0px 10px; + padding:0px; + } + +.context-plugins .context-plugin-selector { + width:25%; + background:#fff; + } + + .context-plugins .context-plugin-list .disabled { display:none; } + + .context-plugins .context-plugin-list ul { + margin:0px; + padding:0px; + } + + .context-plugins .context-plugin-list li { + list-style:none; + list-style-image:none; + background:transparent; + + padding:0px; + margin:0px; + border:0px; + } + + .context-plugins .context-plugin-list li a { + display:block; + position:relative; + padding:5px 10px 4px; + border-bottom:1px solid #e8e8e8; + } + + .context-plugins .context-plugin-list li a.active-form { + background:#f8f8f8; + color:#333; + font-weight:bold; + } + + .context-plugins .context-plugin-list li span.remove { + display:none; + position:absolute; + top:5px; + right:10px; + + font-size:9px; + font-weight:normal; + + -moz-border-radius:5px; + -webkit-border-radius:5px; + padding:0px 5px; + background:#fff; + } + + .context-plugins .context-plugin-list li a:hover span.remove { display:block; } + +.context-plugins .context-plugin-forms { + float:right; + width:75%; + background:#f8f8f8; + min-height:200px; + } + + .context-plugins .context-plugin-forms .context-plugin-form { + padding:10px; + display:none; + } + + .context-plugins .context-plugin-forms .active-form { display:block; } + + .context-plugins .context-plugin-form .form-checkboxes { + max-height:300px; + overflow:auto; + } diff --git a/context_ui/context_ui.info b/context_ui/context_ui.info new file mode 100644 index 0000000..27588b5 --- /dev/null +++ b/context_ui/context_ui.info @@ -0,0 +1,9 @@ +name = Context UI +description = "Provides a simple UI for settings up a site structure using Context." +dependencies[] = context +package = Context +core = "7.x" +configure = admin/structure/context + +files[] = context.module +files[] = tests/context_ui.test diff --git a/context_ui/context_ui.install b/context_ui/context_ui.install new file mode 100644 index 0000000..8e22dc4 --- /dev/null +++ b/context_ui/context_ui.install @@ -0,0 +1,9 @@ + li', this.form).each(function() { + var plugin = $(this).attr('class').split('context-plugin-')[1].split(' ')[0]; + if ($(this).is('.disabled')) { + $('.context-plugin-selector select option[value='+plugin+']', this.form).show(); + } + else { + state.push(plugin); + $('.context-plugin-selector select option[value='+plugin+']', this.form).hide(); + } + }); + // Set the hidden plugin list state. + $('.context-plugin-selector input.context-plugins-state', this.form).val(state.join(',')); + + // Reset the selector. + $('.context-plugin-selector select', this.form).val(0); + return this; + }; + + // Add a plugin to the list. + this.addPlugin = function(plugin) { + $('.context-plugin-list > li.context-plugin-'+plugin, this.form).removeClass('disabled'); + this.showForm(plugin).setState(); + return this; + }; + + // Remove a plugin from the list. + this.removePlugin = function(plugin) { + $('.context-plugin-list > li.context-plugin-'+plugin, this.form).addClass('disabled'); + this.hideForm(plugin).setState(); + return this; + }; + + // Show a plugin form. + this.showForm = function(plugin) { + $('.context-plugin-forms > .context-plugin-form.active-form', this.form).removeClass('active-form'); + $('.context-plugin-forms > .context-plugin-form-'+plugin, this.form).addClass('active-form'); + $('.context-plugin-list > li > a').removeClass('active-form'); + $('.context-plugin-list > li.context-plugin-'+plugin+' > a').addClass('active-form'); + return this; + }; + + // Show a plugin form. + this.hideForm = function(plugin) { + $('.context-plugin-forms > .context-plugin-form-'+plugin, this.form).removeClass('active-form'); + $('.context-plugin-list > li.context-plugin-'+plugin+' > a').removeClass('active-form'); + return this; + }; + + // Select handler. + $('.context-plugin-selector select', this.form).change(function() { + var plugins = $(this).parents('div.context-plugins').data('contextPlugins'); + if (plugins) { + var plugin = $(this).val(); + plugins.addPlugin(plugin); + } + }); + + // Show form handler. + $('.context-plugin-list > li > a', this.form).click(function() { + var plugins = $(this).parents('div.context-plugins').data('contextPlugins'); + if (plugins) { + var plugin = $(this).attr('href').split('#context-plugin-form-')[1]; + plugins.showForm(plugin); + } + return false; + }); + + // Remove handler. + $('.context-plugin-list span.remove', this.form).click(function() { + var plugins = $(this).parents('div.context-plugins').data('contextPlugins'); + if (plugins) { + var plugin = $(this).parent().attr('href').split('#context-plugin-form-')[1]; + plugins.removePlugin(plugin); + } + return false; + }); + + // Set the plugin states. + this.setState(); +} + +Drupal.behaviors.context_ui = { attach: function(context) { + // Initialize context plugin form. + $('form div.context-plugins:not(.context-ui-processed)').each(function() { + $(this).addClass('context-ui-processed'); + $(this).data('contextPlugins', new DrupalContextPlugins($(this))); + }); + + // Initialize context editor. + if ($().pageEditor) { + $('form.context-editor:not(.context-ui-processed)') + .addClass('context-ui-processed') + .pageEditor() + .each(function() { + var editor = $(this); + var defaultContext = $('li.context-editable', this).attr('id').split('context-editable-trigger-')[1]; + $(this).data('defaultContext', defaultContext); + + // Attach start/end handlers to editable contexts. + $('li.context-editable a.edit', editor).click(function() { + var trigger = $(this).parents('li.context-editable').addClass('context-editing'); + var context = trigger.attr('id').split('context-editable-trigger-')[1]; + editor.pageEditor('start', context); + return false; + }); + $('li.context-editable a.done', editor).click(function() { + editor.pageEditor('end'); + return false; + }); + $(editor).submit(function() { + if (editor.pageEditor('isEditing')) { + editor.pageEditor('end'); + } + }); + + // Handler for start event. + editor.bind('start.pageEditor', function(event, context) { + // Fallback to first context if param is empty. + if (!context) { + context = $(this).data('defaultContext'); + $('li#context-editable-trigger-'+context, this).addClass('context-editing'); + } + $(document.body).addClass('context-editing'); + $('#context-editable-'+context, this).show(); + }); + + // Handler for end event. + editor.bind('end.pageEditor', function(event, context) { + $(document.body).removeClass('context-editing'); + $('div.contexts div.context-editable', this).hide(); + $('li.context-editable').removeClass('context-editing'); + $('form.context-editor').addClass('edited'); + }); + }); + } +}}; +})(jQuery); diff --git a/context_ui/context_ui.module b/context_ui/context_ui.module new file mode 100644 index 0000000..2ed2aa6 --- /dev/null +++ b/context_ui/context_ui.module @@ -0,0 +1,441 @@ + 'form', + 'path' => drupal_get_path('module', 'context_ui') .'/theme', + 'template' => 'context-ui-form', + 'file' => 'theme.inc', + ); + $items['context_ui_plugins'] = array( + 'render element' => 'form', + 'path' => drupal_get_path('module', 'context_ui') .'/theme', + 'template' => 'context-ui-plugins', + 'file' => 'theme.inc', + ); + $items['context_ui_editor'] = array( + 'render element' => 'form', + 'path' => drupal_get_path('module', 'context_ui') .'/theme', + 'template' => 'context-ui-editor', + 'file' => 'theme.inc', + ); + return $items; +} + +/** + * Implementation of hook_block_info(). + */ +function context_ui_block_info() { + $blocks = array(); + $blocks['editor'] = array('info' => t('Context editor'), 'admin' => TRUE); + if (module_exists('devel')) { + $blocks['devel'] = array('info' => t('Context inspector'), 'admin' => TRUE); + } + return $blocks; +} + +/** + * Implementation of hook_block_view(). + */ +function context_ui_block_view($delta = '') { + switch ($delta) { + case 'editor': + if (user_access('administer contexts') && strpos($_GET['q'], 'admin/structure/context') === FALSE && $contexts = context_active_contexts()) { + return array( + 'subject' => t('Context editor'), + 'content' => drupal_get_form('context_ui_editor', $contexts), + ); + } + break; + case 'devel': + if (module_exists('devel') && $all = context_get()) { + return array( + 'subject' => t('Context inspector'), + 'content' => kdevel_print_object($all), + ); + } + break; + } +} + +/** + * Implementation of hook_permission(). + */ +function context_ui_permission() { + $permissions = array(); + $permissions['administer contexts'] = array( + 'title' => 'Administer contexts', + 'description' => 'Associate menus, views, blocks, etc. with different contexts to structure your site.' + ); + $permissions['context ajax block access'] = array( + 'title' => t('Access All Blocks'), + 'description' => t('Allows users to access all rendered blocks via an AJAX callback. If you have some blocks that should not be rendered for some users but need those users to be able to use context UI, then implement hook_context_allow_ajax_block_access with the necessary logic.'), + ); + return $permissions; +} + +/** + * Implementation of hook_menu(). + */ +function context_ui_menu() { + $items = array(); + $items['admin/structure/context/settings'] = array( + 'title' => 'Settings', + 'access callback' => 'user_access', + 'access arguments' => array('administer contexts'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('context_ui_settings'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 3, + ); + $items['context-ui/activate'] = array( + 'title' => 'Activate Context UI', + 'access arguments' => array('administer contexts'), + 'page callback' => 'context_ui_activate', + 'type' => MENU_CALLBACK + ); + $items['context-ui/deactivate'] = array( + 'title' => 'Deactivate Context UI', + 'access arguments' => array('administer contexts'), + 'page callback' => 'context_ui_deactivate', + 'type' => MENU_CALLBACK + ); + return $items; +} + +/** + * Implementation of hook_help(). + */ +function context_ui_help($path, $arg) { + switch ($path) { + case 'admin/help#context_ui': + $output = file_get_contents(drupal_get_path('module', 'context_ui') .'/README.txt'); + return module_exists('markdown') ? filter_xss_admin(module_invoke('markdown', 'filter', 'process', 0, -1, $output)) : '
'. check_plain($output) .'
'; + case 'admin/structure/context': + return '

'. t('Context allows you to manage contextual conditions and reactions for different portions of your site. You can think of each context as representing a "section" of your site. For each context, you can choose the conditions that trigger this context to be active and choose different aspects of Drupal that should react to this active context.') .'

'; + } +} + +/** + * Inline context editor form. + */ +function context_ui_editor($form, &$form_state, $contexts) { + $form = array( + '#attributes' => array('class' => array('context-editor')), + '#theme' => array('context_ui_editor'), + 'editables' => array(), + 'contexts' => array('#tree' => TRUE), + 'buttons' => array('#tree' => FALSE), + ); + + $form['title'] = array( + '#prefix' => '

', + '#markup' => t('Select the Context/Layer to Edit'), + '#suffix' => '

', + '#weight' => -2, + ); + + //add some help text to the top of the form + $form['help'] = array ( + '#prefix' => '

', + '#markup' => t('Select which context, or layer of blocks, to edit. + Each context is configured to appear on different sets of pages so read the description carefully. + When you are done editing click Done and save your changes. + You may use the Stop Editing Layout link to close the editor.'), + '#suffix' => '

', + '#weight' => -1, + ); + + $items = array(); + $form_context = array(); + ksort($contexts); + foreach ($contexts as $context) { + $edit = l(t('Edit'), $_GET['q'], array('fragment' => $context->name, 'attributes' => array('class' => array('edit')))); + $done = l(t('Done'), $_GET['q'], array('fragment' => $context->name, 'attributes' => array('class' => array('done')))); + $readable_name = ucwords(str_replace('_', ' ', $context->name)); + $description = empty($context->description) ? '' : + "
".check_plain($context->description)."
"; + $items[] = array( + 'data' => "
" . $readable_name. "
" . $description, + 'class' => array('context-editable clearfix'), + 'id' => "context-editable-trigger-{$context->name}", + ); + $form_context = array( + '#tree' => TRUE, + '#type' => module_exists('admin') ? 'admin_panes' : NULL, + 'context' => array('#type' => 'value', '#value' => $context), + ); + + // Edit context reactions. + foreach (array_keys(context_reactions()) as $reaction) { + $plugin = context_get_plugin('reaction', $reaction); + if (method_exists($plugin, 'editor_form') && ($plugin_form = $plugin->editor_form($context))) { + $form_context["reaction-{$reaction}"] = $plugin_form + array('#title' => $plugin->title); + } + } + + // Add to main form. + $form['contexts'][$context->name] = $form_context; + } + + // Display editable contexts in list. + $form['editables']['#markup'] = theme('item_list', array('items' => $items)); + + // Buttons. + $form['buttons']['save'] = array( + '#type' => 'submit', + '#value' => t('Save changes'), + '#submit' => array('context_ui_editor_submit'), + ); + $form['buttons']['cancel'] = array( + '#type' => 'submit', + '#value' => t('Reset'), + '#submit' => array('context_ui_editor_cancel'), + ); + + $form['stop'] = array( + '#markup' => l(t('Stop Editing Layout'), 'context-ui/deactivate', array( + 'query' => array('destination' => current_path()), + 'attributes' => array('class' => array('context_ui_dialog-stop')), + ) + ), + ); + + return $form; +} + +/** + * Values processor for context_ui_editor_submit(). + * Split out for reuse by overriding submit handlers. + */ +function context_ui_editor_process($values) { + $context = $values['context']; + foreach (array_keys(context_conditions()) as $condition) { + if (isset($values['condition'][$condition])) { + $plugin = context_get_plugin('condition', $condition); + if ($plugin && method_exists($plugin, 'editor_form_submit')) { + $context->conditions[$condition]['values'] = $plugin->editor_form_submit($context, $values['condition'][$condition]); + } + } + if (isset($context->conditions[$condition]) && context_empty($context->conditions[$condition]['values'])) { + unset($context->conditions[$condition]); + } + } + foreach (array_keys(context_reactions()) as $reaction) { + if (isset($values["reaction-{$reaction}"])) { + $plugin = context_get_plugin('reaction', $reaction); + if ($plugin && method_exists($plugin, 'editor_form_submit')) { + $context->reactions[$reaction] = $plugin->editor_form_submit($context, $values["reaction-{$reaction}"]); + } + } + if (isset($context->reactions[$reaction]) && context_empty($context->reactions[$reaction])) { + unset($context->reactions[$reaction]); + } + } + return $context; +} + +/** + * Save handler for context_block_editor(). + */ +function context_ui_editor_submit(&$form, &$form_state) { + foreach ($form_state['values']['contexts'] as $name => $values) { + $original_reactions = var_export($values['context']->reactions, TRUE); + $context = context_ui_editor_process($values); + //compare string values instead of actual objects to avoid problems with aliasing + if (($original_reactions !== var_export($context->reactions, TRUE))) { + if (context_save($context)) { + drupal_set_message(t('Saved %title.', array( + '%title' => (!empty($context->description) ? $context->description : $context->name) + ))); + } + else { + drupal_set_message(t('Could not save context %title.', array('%title' => $context->name)), 'error'); + } + } + } + return; +} + +/** + * Cancel handler for context_block_editor(). + */ +function context_ui_editor_cancel(&$form, &$form_state) { + return; +} + +/** + * Settings form. + */ +function context_ui_settings($form, &$form_state) { + $form = array(); + foreach (context_conditions() as $condition => $info) { + if ($plugin = context_get_plugin('condition', $condition)) { + $settings_form = $plugin->settings_form(); + if ($settings_form) { + $form['conditions'][$condition] = $settings_form; + $form['conditions'][$condition]['#tree'] = FALSE; + $form['conditions'][$condition]['#type'] = 'fieldset'; + $form['conditions'][$condition]['#title'] = $info['title']; + } + } + } + foreach (context_reactions() as $reaction => $info) { + if ($plugin = context_get_plugin('reaction', $reaction)) { + $settings_form = $plugin->settings_form(); + if ($settings_form) { + $form['reactions'][$reaction] = $settings_form; + $form['reactions'][$reaction]['#tree'] = FALSE; + $form['reactions'][$reaction]['#type'] = 'fieldset'; + $form['reactions'][$reaction]['#title'] = $info['title']; + } + } + } + $form['context_ui_dialog_enabled'] = array( + '#type' => 'checkbox', + '#title' => t('Use Context Editor Dialog'), + '#default_value' => context_ui_dialog_is_enabled(), + '#description' => t('When enabled all contextual links will have a Edit Layout link that will refresh the page with the context editor in a dialog box.'), + ); + $form = system_settings_form($form); + $form['#submit'][] = 'context_ui_settings_submit'; + return $form; +} + +/** + * Extra submit handler for context_ui_settings. + * Mark the menu cache as needing a rebuild. + */ +function context_ui_settings_submit($form, &$form_state) { + variable_set('menu_rebuild_needed', TRUE); +} + + +/** + * context_ui_dialog_is_enabled test if the dialog is enabled + */ +function context_ui_dialog_is_enabled() { + + return variable_get("context_ui_dialog_enabled", FALSE); +} + +/** + * Implementation of hook_page_alter(). + * + * If we have the dialog enabled and active build the dialog + * and add to the page + */ +function context_ui_page_alter(&$page) { + $contexts = context_active_contexts(); + if ( + context_ui_dialog_is_enabled() && + context_isset('context_ui', 'context_ui_editor_present') + ) { + $contexts = context_active_contexts(); + $form = drupal_get_form('context_ui_editor', $contexts); + + $path = drupal_get_path('module', 'context_ui'); + drupal_add_library('system', 'ui.dialog'); + drupal_add_js($path . '/context_ui_dialog.js', array('type' => 'file', 'weight' => 50)); + drupal_add_css($path . '/context_ui_dialog.css'); + + //figure out which region to put it in - allow it to be configured for themes using different regions + $placement = variable_get('context_ui_editor_block_region', 'content'); + $page[$placement]['context_ui_editor'] = array( + 0 => array( + '#type' => 'markup', + '#markup' => '', + ), + ); + } +} + +/** + * Implementation of hook_menu_contextual_links_alter(). + * + * we we have the dialog enabled lets add a link to all contextual links + * to activate it. + */ +function context_ui_menu_contextual_links_alter(&$links, $router_item, $root_path) { + if(context_ui_dialog_is_enabled() && + !context_isset('context_ui', 'context_ui_editor_present') && user_access('administer contexts')) { + $links['layout'] = array( + 'href' => 'context-ui/activate', + 'title' => t('Configure Layout'), + 'localized_options' => array( + 'query' => array('destination'=> $_GET['q']), + 'options' => array('html' => FALSE, 'attributes' => array()), + ), + ); + } +} + +/** + * A page call back to activate the context_ui inline editor dialog. + */ +function context_ui_activate() { + if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) { + $_SESSION['context_ui_active'] = $_GET['destination']; + drupal_goto($_GET['destination']); + } +} + +/** + * A page call back to deactivate the context_ui inline editor dialog. + * This is semi unecessary as context editor will auto deactivate upon going to any + * page other than the destination from the start. However, its useful as a place + * to navigate to when deactivating context_ui_editor + */ +function context_ui_deactivate() { + if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) { + $_SESSION['context_ui_active'] = FALSE; + drupal_goto($_GET['destination']); + } +} + +/** + * Implemenation of hook_init(). + * + * If the session says we should have an active dialog set a context variable to tell everything else + * Ignores ajax requests. + */ +function context_ui_init() { + if (!empty($_SESSION['context_ui_active'])) { + $path = $_SESSION['context_ui_active']; + if( $path == request_path() || $path == drupal_get_path_alias() || $path == drupal_get_normal_path(request_path()) ) { + context_set('context_ui', 'context_ui_editor_present', TRUE); + } + } + // Turn off functionality has been moved to hook_page_build() to prevent non-pages from triggering it +} + +/** + * Implementation of hook_page_build(). + * Turn off the context_ui functionality if we move to a different page + */ +function context_ui_page_build(&$page) { + if (!context_get('context_ui', 'context_ui_editor_present') && isset($_SESSION['context_ui_active'])) { + $_SESSION['context_ui_active'] = FALSE; + } +} + +/** + * Ajax callback to get the list of available blocks + * + */ +function context_ui_get_available_blocks() { + drupal_json_output(array('lols' => 'testing')); +} diff --git a/context_ui/context_ui_dialog.css b/context_ui/context_ui_dialog.css new file mode 100644 index 0000000..8e66936 --- /dev/null +++ b/context_ui/context_ui_dialog.css @@ -0,0 +1,107 @@ +#context_ui_dialog-context-ui .item-list { + overflow-y: auto; + overflow-x: hidden; + padding-right: 5px; +} +#context_ui_dialog-context-ui a, #context_ui_dialog-context-ui li a.active { + color: #222; +} +#context_ui_dialog-context-ui ul { + margin: 0; +} +#context_ui_dialog-context-ui ul li { + padding: 4px 0; + list-style: none; +} + +#context_ui_dialog-context-ui .buttons { + padding-top:15px; +} + +#context_ui_dialog-context-ui div.admin-pane-condition, +#context_ui_dialog-context-ui div.admin-pane-reaction-theme, +#context_ui_dialog-context-ui div.admin-pane-reaction-theme_html { + display: none; +} + +.boxes-box-editing { + background: none repeat scroll 0 0 #EEEEDD; + display: inline-block; + padding: 3px; +} + +body.context-editing div.contextual-links-wrapper { + right: 0; + top: 40px; +} + +#context_ui_dialog-context-ui a.context_ui_dialog-stop, +#context_ui_dialog-context-ui a:link.context_ui_dialog-stop, +#context_ui_dialog-context-ui a:visited.context_ui_dialog-stop { + float:right; + padding-top:18px; + color: #ad3f00; +} + +form.context-editing li.context-editable { + opacity: .2; +} +form.context-editing li.context-editing { + visibility : visible; + display : list-item; + opacity: 1; +} +#context_ui_dialog-shadow { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background-color: #fff; + /* For IE 8 */ + -ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#555555')"; + /* For IE 5.5 - 7 */ + filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#555555'); +} +#context_ui_dialog-context-ui { + width: 550px; + position:fixed; + background:#fff; + color:#222; + top:140px; + left: 0px; + display:none; + z-index:400; + padding:10px; + font-weight:bold; + border:1px solid #ddd; + border-left:0; + border-radius:0 6px 6px 0; + /* Shadow */ + -moz-box-shadow: 3px 3px 4px #555; + -webkit-box-shadow: 3px 3px 4px #555; + box-shadow: 3px 3px 4px #555; +} + +.context-ui-dialog-open { + border-radius:0 6px 6px 0; + position: absolute; + background:#fff; + color:#222; + top: 50px; + padding: 10px; + width: 55px; + right:-76px; + border:1px solid #ddd; + border-left:0; + font-size:12px; + /* Shadow */ + -moz-box-shadow: 3px 3px 4px #555; + -webkit-box-shadow: 3px 3px 4px #555; + box-shadow: 3px 3px 4px #555; + /* For IE 8 */ + -ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#555555')"; + /* For IE 5.5 - 7 */ + filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#555555'); +} diff --git a/context_ui/context_ui_dialog.js b/context_ui/context_ui_dialog.js new file mode 100644 index 0000000..dc73f91 --- /dev/null +++ b/context_ui/context_ui_dialog.js @@ -0,0 +1,47 @@ +(function ($) { + + Drupal.behaviors.context_ui_dialog = { + attach: function(context) { + var selector = $('#context_ui_dialog-context-ui', context).not('context_ui_dialog-processed'); + + if(selector) { + selector.addClass('context_ui_dialog-processed'); + selector.detach(); + $('#page').prepend(selector); + + var labelOpen = Drupal.t('Select Context'); + var labelClose = Drupal.t('Hide'); + + // Create a tab to show/hide our edit area + var tab = $(''+labelClose+''); + selector.append(tab); + + selector.toggled = false; + var width = $(selector).outerWidth(); + tab.click(function(e){ + if(selector.toggled) { + selector.stop(true, false).animate({'left':0}, 400); + selector.toggled = false; + $(this).text(labelClose); + } else { + selector.stop(true, false).animate({'left':-width-4}, 400); + selector.toggled = true; + $(this).text(labelOpen); + } + }); + + $('#context_ui_dialog-context-ui').show(); + + // Make sure the UI is 60% of the size of the window + var context_ui_height = Math.round(6 * $(window).height() / 10); + var item_list_height = context_ui_height - 200; + item_list_height = (item_list_height < 50) ? 50 : item_list_height; + $('#context_ui_dialog-context-ui').height(context_ui_height); + $('#context_ui_dialog-context-ui .item-list').height(item_list_height); + + // Add a class to body + $('body').once().addClass('context-field-editor'); + } + } + }; +})(jQuery); diff --git a/context_ui/export_ui/context.inc b/context_ui/export_ui/context.inc new file mode 100644 index 0000000..9dcab78 --- /dev/null +++ b/context_ui/export_ui/context.inc @@ -0,0 +1,24 @@ + 'administer contexts', + 'schema' => 'context', + 'menu' => array( + 'menu prefix' => 'admin/structure', + 'menu item' => 'context', + 'menu title' => 'Context', + 'menu description' => 'Associate menus, views, blocks, etc. with different contexts to structure your site.', + ), + 'title singular' => t('context'), + 'title singular proper' => t('Context'), + 'title plural' => t('contexts'), + 'title plural proper' => t('Contexts'), + 'form' => array( + 'settings' => 'context_ui_form', + 'submit' => 'context_ui_form_submit', + ), + 'handler' => array( + 'class' => 'context_export_ui', + 'parent' => 'ctools_export_ui', + ), +); diff --git a/context_ui/export_ui/context_export_ui.class.php b/context_ui/export_ui/context_export_ui.class.php new file mode 100644 index 0000000..db5cf86 --- /dev/null +++ b/context_ui/export_ui/context_export_ui.class.php @@ -0,0 +1,327 @@ + $this->list_table_header(), + 'rows' => $this->rows, + 'attributes' => array( + 'class' => array('context-admin'), + 'id' => 'ctools-export-ui-list-items', + ), + ); + return theme('table', $table); + } + + function list_build_row($item, &$form_state, $operations) { + $name = $item->name; + + // Add a row for tags. + $tag = !empty($item->tag) ? $item->tag : t('< Untagged >'); + if (!isset($this->rows[$tag])) { + $this->rows[$tag]['data'] = array(); + $this->rows[$tag]['data'][] = array('data' => check_plain($tag), 'colspan' => 3, 'class' => array('tag')); + $this->sorts["{$tag}"] = $tag; + } + + // Build row for each context item. + $this->rows["{$tag}:{$name}"]['data'] = array(); + $this->rows["{$tag}:{$name}"]['class'] = !empty($item->disabled) ? array('ctools-export-ui-disabled') : array('ctools-export-ui-enabled'); + $this->rows["{$tag}:{$name}"]['data'][] = array( + 'data' => check_plain($name) . "
" . check_plain($item->description) . "
", + 'class' => array('ctools-export-ui-name') + ); + $this->rows["{$tag}:{$name}"]['data'][] = array( + 'data' => check_plain($item->type), + 'class' => array('ctools-export-ui-storage') + ); + $this->rows["{$tag}:{$name}"]['data'][] = array( + 'data' => theme('links', array( + 'links' => $operations, + 'attributes' => array('class' => array('links inline')) + )), + 'class' => array('ctools-export-ui-operations'), + ); + + // Sort by tag, name. + $this->sorts["{$tag}:{$name}"] = $tag . $name; + } + + /** + * Override of edit_form_submit(). + * Don't copy values from $form_state['values']. + */ + function edit_form_submit(&$form, &$form_state) { + if (!empty($this->plugin['form']['submit'])) { + $this->plugin['form']['submit']($form, $form_state); + } + context_invalidate_cache(); + } + + /** + * Override default final validation for ctools. With import wizard + * it was possible to get default ctools export ui name validation + * rules, this ensures we always get ours. + */ + function edit_finish_validate(&$form, &$form_state) { + if ($form_state['op'] != 'edit') { + // Validate the name. Fake an element for form_error(). + $export_key = $this->plugin['export']['key']; + $element = array( + '#value' => $form_state['item']->{$export_key}, + '#parents' => array('name'), + ); + $form_state['plugin'] = $this->plugin; + context_ui_edit_name_validate($element, $form_state); + } + } +} + + +/** + * Generates the omnibus context definition editing form. + * + * @param $form + * Form array to populate. + * @param $form_state + * Form state array + */ +function context_ui_form(&$form, &$form_state) { + $conditions = array_keys(context_conditions()); + sort($conditions); + $reactions = array_keys(context_reactions()); + sort($reactions); + + $context = $form_state['item']; + if (!empty($form_state['input'])) { + $context = _context_ui_rebuild_from_input($context, $form_state['input'], $conditions, $reactions); + } + + $form['#base'] = 'context_ui_form'; + $form['#theme'] = 'context_ui_form'; + + // Core context definition + $form['info']['#type'] = 'fieldset'; + $form['info']['#tree'] = FALSE; + + + $form['info']['name']['#element_validate'] = array('context_ui_edit_name_validate'); + + $form['info']['tag'] = array( + '#title' => t('Tag'), + '#type' => 'textfield', + '#required' => FALSE, + '#maxlength' => 255, + '#default_value' => isset($context->tag) ? $context->tag : '', + '#description' => t('Example: theme') .'
'. t('A tag to group this context with others.'), + ); + + $form['info']['description'] = array( + '#title' => t('Description'), + '#type' => 'textfield', + '#required' => FALSE, + '#maxlength' => 255, + '#default_value' => isset($context->description) ? $context->description: '', + '#description' => t('The description of this context definition.'), + ); + + // Condition mode + $form['condition_mode'] = array( + '#type' => 'checkbox', + '#default_value' => isset($context->condition_mode) ? $context->condition_mode : FALSE, + '#title' => t('Require all conditions'), + '#description' => t('If checked, all conditions must be met for this context to be active. Otherwise, the first condition that is met will activate this context.') + ); + + // Condition plugin forms + $form['conditions'] = array( + '#theme' => 'context_ui_plugins', + '#title' => t('Conditions'), + '#description' => t('Trigger the activation of this context'), + '#tree' => TRUE, + 'selector' => array( + '#type' => 'select', + '#options' => array(0 => '<'. t('Add a condition') .'>'), + '#default_value' => 0, + ), + 'state' => array( + '#attributes' => array('class' => array('context-plugins-state')), + '#type' => 'hidden', + ), + 'plugins' => array('#tree' => TRUE), + ); + foreach ($conditions as $condition) { + if ($plugin = context_get_plugin('condition', $condition)) { + $form['conditions']['plugins'][$condition] = array( + '#tree' => TRUE, + '#plugin' => $plugin, + '#context_enabled' => isset($context->conditions[$condition]), // This flag is used at the theme layer. + 'values' => $plugin->condition_form($context), + 'options' => $plugin->options_form($context), + ); + $form['conditions']['selector']['#options'][$condition] = $plugin->title; + } + } + + // Reaction plugin forms + $form['reactions'] = array( + '#theme' => 'context_ui_plugins', + '#title' => t('Reactions'), + '#description' => t('Actions to take when this context is active'), + '#tree' => TRUE, + 'selector' => array( + '#type' => 'select', + '#options' => array(0 => '<'. t('Add a reaction') .'>'), + '#default_value' => 0, + ), + 'state' => array( + '#attributes' => array('class' => array('context-plugins-state')), + '#type' => 'hidden', + ), + 'plugins' => array('#tree' => TRUE), + ); + foreach ($reactions as $reaction) { + if ($plugin = context_get_plugin('reaction', $reaction)) { + $form['reactions']['plugins'][$reaction] = $plugin->options_form($context) + array( + '#plugin' => $plugin, + '#context_enabled' => isset($context->reactions[$reaction]), // This flag is used at the theme layer. + ); + $form['reactions']['selector']['#options'][$reaction] = $plugin->title; + } + } +} + +/** + * Handle the complex job of rebuilding a Context from submission data in the case of a validation error. + * + * @param $context + * The context object to modify. + * @param $input + * A form submission values + * @param $conditions + * The full list of condition plugins + * @param $reactions + * The full list of reaction plugins + * + * @return + * A context object + */ +function _context_ui_rebuild_from_input($context, $input, $conditions, $reactions) { + $condition_defaults = array(); + foreach ($conditions as $condition) { + if ($plugin = context_get_plugin('condition', $condition)) { + $condition_defaults[$condition] = array( + 'values' => $plugin->condition_form($context), + 'options' => $plugin->options_form($context), + ); + } + } + $input['conditions']['plugins'] = array_merge($condition_defaults, $input['conditions']['plugins']); + + $reaction_defaults = array(); + foreach ($reactions as $reaction) { + if ($plugin = context_get_plugin('reaction', $reaction)) { + $reaction_defaults[$reaction] = $plugin->options_form($context); + } + } + $input['reactions']['plugins'] = array_merge($reaction_defaults, $input['reactions']['plugins']); + + return context_ui_form_process($context, $input, FALSE); +} + +/** + * Modifies a context object from submitted form values. + * + * @param $context + * The context object to modify. + * @param $form + * A form array with submitted values + * @param $submit + * A flag indicating if we are building a context on submit. If on + * submit, it will clear out conditions/reactions that are empty. + * + * @return + * A context object + */ +function context_ui_form_process($context, $form, $submit = TRUE) { + $context->name = isset($form['name']) ? $form['name'] : $context->name; + $context->description = isset($form['description']) ? $form['description'] : NULL; + $context->tag = isset($form['tag']) ? $form['tag'] : NULL; + $context->condition_mode = isset($form['condition_mode']) ? $form['condition_mode'] : NULL; + $context->conditions = array(); + $context->reactions = array(); + if (!empty($form['conditions'])) { + $enabled = explode(',', $form['conditions']['state']); + foreach ($form['conditions']['plugins'] as $condition => $values) { + if (in_array($condition, $enabled, TRUE) && ($plugin = context_get_plugin('condition', $condition))) { + if (isset($values['values'])) { + $context->conditions[$condition]['values'] = $plugin->condition_form_submit($values['values']); + } + if (isset($values['options'])) { + $context->conditions[$condition]['options'] = $plugin->options_form_submit($values['options']); + } + if ($submit && context_empty($context->conditions[$condition]['values'])) { + unset($context->conditions[$condition]); + } + } + } + } + if (!empty($form['reactions'])) { + $enabled = explode(',', $form['reactions']['state']); + foreach ($form['reactions']['plugins'] as $reaction => $values) { + if (in_array($reaction, $enabled, TRUE) && ($plugin = context_get_plugin('reaction', $reaction))) { + if (isset($values)) { + $context->reactions[$reaction] = $plugin->options_form_submit($values); + } + if ($submit && context_empty($context->reactions[$reaction])) { + unset($context->reactions[$reaction]); + } + } + } + } + return $context; +} + +/** + * Submit handler for main context_ui form. + */ +function context_ui_form_submit($form, &$form_state) { + $form_state['item'] = context_ui_form_process($form_state['item'], $form_state['values']); +} + +/** + * Replacement for ctools_export_ui_edit_name_validate(). Allow dashes. + */ +function context_ui_edit_name_validate($element, &$form_state) { + $plugin = $form_state['plugin']; + // Check for string identifier sanity + if (!preg_match('!^[a-z0-9_-]+$!', $element['#value'])) { + form_error($element, t('The name can only consist of lowercase letters, underscores, dashes, and numbers.')); + return; + } + + // Check for name collision + if ($form_state['op'] != 'edit') { + if (empty($form_state['item']->export_ui_allow_overwrite) && $exists = ctools_export_crud_load($plugin['schema'], $element['#value'])) { + form_error($element, t('A @plugin with this name already exists. Please choose another name or delete the existing item before creating a new one.', array('@plugin' => $plugin['title singular']))); + } + } +} diff --git a/context_ui/jquery.pageEditor.js b/context_ui/jquery.pageEditor.js new file mode 100644 index 0000000..9b1392e --- /dev/null +++ b/context_ui/jquery.pageEditor.js @@ -0,0 +1,38 @@ + +/** + * Generic pageEditor plugin. Allows an editor DOM object to trigger + * init, start, and end events. Implementors can check whether the + * editor is currently editing and bind handlers for the events triggered + * by the editor. + */ +(function($) { + $.fn.pageEditor = function(method, data) { + this.each(function() { + switch (method) { + case 'isEditing': + return this.editing; + case 'start': + if (!this.inited) { + this.inited = true; + $(this).trigger('init.pageEditor', data); + } + this.editing = true; + $(this).trigger('start.pageEditor', data); + break; + case 'end': + if (!this.inited) { + this.inited = true; + $(this).trigger('init.pageEditor', data); + } + this.editing = false; + $(this).trigger('end.pageEditor', data); + break; + default: + this.inited = false; + this.editing = false; + break; + } + }); + return this; + }; +})(jQuery); diff --git a/context_ui/json2.js b/context_ui/json2.js new file mode 100644 index 0000000..d4f4541 --- /dev/null +++ b/context_ui/json2.js @@ -0,0 +1 @@ +if(!this.JSON){this.JSON={}}(function(){function f(n){return n<10?'0'+n:n}if(typeof Date.prototype.toJSON!=='function'){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+'-'+f(this.getUTCMonth()+1)+'-'+f(this.getUTCDate())+'T'+f(this.getUTCHours())+':'+f(this.getUTCMinutes())+':'+f(this.getUTCSeconds())+'Z':null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==='string'?c:'\\u'+('0000'+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==='object'&&typeof value.toJSON==='function'){value=value.toJSON(key)}if(typeof rep==='function'){value=rep.call(holder,key,value)}switch(typeof value){case'string':return quote(value);case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null'}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==='[object Array]'){length=value.length;for(i=0;i 'Context UI functional tests', + 'description' => 'Create and save a context.', + 'group' => 'Context UI', + ); + } + + function setUp() { + parent::setUp('ctools', 'context', 'context_ui', 'blog', 'menu'); + + // Create and login user + $admin_user = $this->drupalCreateUser(array( + 'access administration pages', + 'administer site configuration', + 'administer contexts', + 'access content', + 'create blog content' + )); + $this->drupalLogin($admin_user); + } + + function testCreateContext() { + // Create context + $context = new stdClass(); + $context->name = strtolower($this->randomName(15)); + $context->description = strtolower($this->randomName(15)); + $context->tag = strtolower($this->randomName(15)); + $this->context = $context; + + $this->drupalGet('admin'); + $this->drupalGet('admin/structure'); + $this->drupalGet('admin/structure/context'); + + $edit = array( + 'name' => $context->name, + 'description' => $context->description, + 'tag' => $context->tag, + 'conditions[plugins][node][values][blog]' => 'blog', + 'reactions[plugins][menu]' => 'node/add/blog', + ); + $this->drupalPost('admin/structure/context/add', $edit, 'Save'); + $this->assertText($context->name . ' has been created.', 'Context saved.'); + + $edit = array(); + // export_ui confirm delete + $this->drupalPost('admin/structure/context/list/' . $context->name . '/edit', $edit, 'Delete'); + $this->assertTrue(strpos($this->getUrl(), 'admin/structure/context/list/' . $context->name . '/delete') !== FALSE, 'Context deletion confirmation page displayed'); + } +} diff --git a/context_ui/theme/context-ui-editor.tpl.php b/context_ui/theme/context-ui-editor.tpl.php new file mode 100644 index 0000000..fb1e2ea --- /dev/null +++ b/context_ui/theme/context-ui-editor.tpl.php @@ -0,0 +1,10 @@ +
+ +
+ +
+ + +
+
+
diff --git a/context_ui/theme/context-ui-form.tpl.php b/context_ui/theme/context-ui-form.tpl.php new file mode 100644 index 0000000..5a726fd --- /dev/null +++ b/context_ui/theme/context-ui-form.tpl.php @@ -0,0 +1,2 @@ + +
diff --git a/context_ui/theme/context-ui-plugins.tpl.php b/context_ui/theme/context-ui-plugins.tpl.php new file mode 100644 index 0000000..f2519bf --- /dev/null +++ b/context_ui/theme/context-ui-plugins.tpl.php @@ -0,0 +1,22 @@ +
+
+ +
+ +
+ +
+ +
+
+

+
+ + +
+ $plugins, 'attributes' => array('class' => array('context-plugin-list')))) ?> +
+ + + +
diff --git a/context_ui/theme/filter.js b/context_ui/theme/filter.js new file mode 100644 index 0000000..befd5b7 --- /dev/null +++ b/context_ui/theme/filter.js @@ -0,0 +1,66 @@ +/** + * create a simple search filter thing for a list + */ +(function ($) { + Drupal.Filter = function (list, title, type, parent){ + this.list = list; + this.title = title; + //provide defaults for type and parent so bad things don't happen + if (!type) { var type = '*'; } + this.type = type; + if (!parent) { var parent = list; } + this.parent = parent; + + this.init(); + } + + Drupal.Filter.prototype = { + init : function(){ + this.wrapper = $('
'); + if(this.title){ + this.title = '

' + this.title + '

'; + this.wrapper.append(this.title); + } + this.input = $(''); + this.wrapper.append(this.input); + + $(this.parent).append(this.wrapper); + this.createHandlers(); + }, + createHandlers : function(){ + var self = this; + $(this.input).keyup(function(e){ + self.filter(); + }); + }, + filter : function(){ + //show all first off + $('*', this.list).show(); + //hide ignored items + if(this.input.val()) { + $('*', this.list).not(this.type).hide(); + } + + var regex = new RegExp(this.input.val(), 'i'); + + var self = this; + $(this.type, this.list).each(function(ind, el) { + var string = self.strip(el.innerHTML); + if(!regex.test(string)){ + $(el).hide(); + } else { //show the parent and any labels or whatever in the parent + var parent = $(el).parent().show(); + $('*', parent).not(self.type).show(); + } + }); + }, + strip : function(string){ + var strip = /<([^<|^>]*)>/i; + while(strip.test(string)){ + var matches = string.match(strip); + string = string.replace(strip, ''); + } + return string; + } + }; +})(jQuery); \ No newline at end of file diff --git a/context_ui/theme/theme.inc b/context_ui/theme/theme.inc new file mode 100644 index 0000000..8004e9e --- /dev/null +++ b/context_ui/theme/theme.inc @@ -0,0 +1,54 @@ + $vars['form']['plugins'][$plugin]['#plugin']->title . "" . t('Remove') . "", + 'href' => $_GET['q'], + 'html' => TRUE, + 'fragment' => "context-plugin-form-{$plugin}", + ); + $class = $vars['form']['plugins'][$plugin]['#context_enabled'] ? "context-plugin-{$plugin}" : "context-plugin-{$plugin} disabled"; + $vars['plugins'][$class] = $link; + } +} + +/** + * Preprocessor for theme('context_ui_form'). + */ +function template_preprocess_context_ui_form(&$vars) { + drupal_add_css(drupal_get_path("module", "context_ui") . "/context_ui.css"); + drupal_add_js(drupal_get_path("module", "context_ui") . "/context_ui.js"); + drupal_add_js(drupal_get_path('module', 'context_ui') . '/jquery.pageEditor.js'); + $vars['buttons'] = $vars['form']['buttons']; + unset($vars['form']['buttons']); +} diff --git a/plugins/context_condition.inc b/plugins/context_condition.inc new file mode 100644 index 0000000..95951d9 --- /dev/null +++ b/plugins/context_condition.inc @@ -0,0 +1,196 @@ + $val) { + if (is_object($val) || (is_array($val))) { + $this->{$key} = unserialize(serialize($val)); + } + } + } + + /** + * Constructor. Do not override. + */ + function __construct($plugin, $info) { + $this->plugin = $plugin; + $this->title = isset($info['title']) ? $info['title'] : $plugin; + $this->description = isset($info['description']) ? $info['description'] : ''; + } + + /** + * Condition values. + */ + function condition_values() { + return array(); + } + + /** + * Condition form. + */ + function condition_form($context) { + return array( + '#title' => $this->title, + '#description' => $this->description, + '#options' => $this->condition_values(), + '#type' => 'checkboxes', + '#default_value' => $this->fetch_from_context($context, 'values'), + ); + } + + /** + * Condition form submit handler. + */ + function condition_form_submit($values) { + ksort($values); + + // Editor forms are generally checkboxes -- do some checkbox processing. + return drupal_map_assoc(array_keys(array_filter($values))); + } + + /** + * Options form. Provide additional options for your condition. + */ + function options_form($context) { + return array(); + } + + /** + * Options form submit handler. + */ + function options_form_submit($values) { + return $values; + } + + /** + * Settings form. Provide variable settings for your condition. + */ + function settings_form() { + return array(); + } + + /** + * Context editor form for conditions. + */ + function editor_form($context = NULL) { + $form = array(); + if (!empty($this->values)) { + $options = $this->condition_values(); + foreach ($this->values as $value => $contexts) { + $label = "{$this->title}: "; + $label .= isset($options[$value]) ? trim($options[$value], ' -') : $value; + $form[$value] = array( + '#type' => 'checkbox', + '#title' => check_plain($label), + '#default_value' => empty($context->name) ? TRUE : in_array($context->name, $contexts, TRUE), + ); + } + } + return $form; + } + + /** + * Context editor form submit handler. + */ + function editor_form_submit(&$context, $values) { + // Merge existing values in from non-active conditions. + $existing = $this->fetch_from_context($context, 'values'); + $values += !empty($existing) ? $existing : array(); + + ksort($values); + + // Editor forms are generally checkboxes -- do some checkbox processing. + return drupal_map_assoc(array_keys(array_filter($values))); + } + + /** + * Public method that is called from hooks or other integration points with + * Drupal. Note that it is not implemented in the base class, allowing + * extending classes to change the function signature if necessary. + * + * function execute($value) { + * foreach ($this->get_contexts($value) as $context) { + * $this->condition_met($context, $value); + * } + * } + */ + + /** + * Marks a context as having met this particular condition. + */ + function condition_met($context, $value = NULL) { + if (isset($value)) { + $this->values[$value] = isset($this->values[$value]) ? $this->values[$value] : array(); + $this->values[$value][] = $context->name; + } + context_condition_met($context, $this->plugin); + } + + /** + * Check whether this condition is used by any contexts. Can be used to + * prevent expensive condition checks from being triggered when no contexts + * use this condition. + */ + function condition_used() { + $map = context_condition_map(); + return !empty($map[$this->plugin]); + } + + /** + * Retrieve all contexts with the condition value provided. + */ + function get_contexts($value = NULL) { + $map = context_condition_map(); + $map = isset($map[$this->plugin]) ? $map[$this->plugin] : array(); + + $contexts = array(); + if (isset($value) && (is_string($value) || is_numeric($value))) { + if (isset($map[$value]) && is_array($map[$value])) { + foreach ($map[$value] as $name) { + if (!isset($contexts[$name])) { + $context = context_load($name); + $contexts[$context->name] = $context; + } + } + } + } + else { + foreach ($map as $submap) { + foreach ($submap as $name) { + if (!isset($contexts[$name])) { + $context = context_load($name); + $contexts[$context->name] = $context; + } + } + } + } + return $contexts; + } + + /** + * Retrieve options from the context provided. + */ + function fetch_from_context($context, $key = NULL) { + if (isset($key)) { + return isset($context->conditions[$this->plugin][$key]) ? $context->conditions[$this->plugin][$key] : array(); + } + return isset($context->conditions[$this->plugin]) ? $context->conditions[$this->plugin] : array(); + } +} diff --git a/plugins/context_condition_book.inc b/plugins/context_condition_book.inc new file mode 100644 index 0000000..24fe769 --- /dev/null +++ b/plugins/context_condition_book.inc @@ -0,0 +1,22 @@ +book, $node->book['menu_name'])) { + foreach ($this->get_contexts($node->book['menu_name']) as $context) { + $this->condition_met($context, $node->book['menu_name']); + } + } + } +} diff --git a/plugins/context_condition_bookroot.inc b/plugins/context_condition_bookroot.inc new file mode 100644 index 0000000..200326c --- /dev/null +++ b/plugins/context_condition_bookroot.inc @@ -0,0 +1,19 @@ +condition_used() && !empty($node->book['bid'])) { + $type = db_select('node') + ->fields('node', array('type')) + ->condition('nid', $node->book['nid']) + ->execute() + ->fetchField(); + $book = new stdClass(); + $book->type = $type; + parent::execute($book, $op); + } + } +} diff --git a/plugins/context_condition_context.inc b/plugins/context_condition_context.inc new file mode 100644 index 0000000..aca0e6a --- /dev/null +++ b/plugins/context_condition_context.inc @@ -0,0 +1,65 @@ +condition_used()) { + $active_contexts = array_keys(context_active_contexts()); + foreach ($this->get_contexts() as $context) { + if (!in_array($context->name, $active_contexts, TRUE) && $values = $this->fetch_from_context($context, 'values')) { + // Always check against the active contexts. + if ($this->match(array_keys(context_active_contexts()), $values)) { + $this->condition_met($context); + } + } + } + // If the list of active contexts has changed, we need to recurse. + if ($active_contexts != array_keys(context_active_contexts())) { + $this->execute(); + } + } + } + + /** + * Retrieve all context conditions. + * + * This method is slightly adapted to context_condition::get_contexts() in + * order to ensure that a context that is used as condition in another context + * gets handled before. + */ + function get_contexts($value = NULL) { + $map = context_condition_map(); + $map = isset($map[$this->plugin]) ? $map[$this->plugin] : array(); + + $contexts = array(); + + // Add the contexts that are needed for conditions in the other contexts + // first. Start with the negated ones first, as we can not unset a met + // condition afterwards. + krsort($map); + foreach ($map as $key => $submap) { + // Negated context conditions start with a "~". + if (substr($key, 0, 1) == "~") { + $key = substr($key, 1); + } + if (!isset($contexts[$key])) { + $context = context_load($key); + // Check if context exists. This will fail for wildcards. + if ($context) { + $contexts[$context->name] = $context; + } + } + } + foreach ($map as $key => $submap) { + foreach ($submap as $name) { + if (!isset($contexts[$name])) { + $context = context_load($name); + $contexts[$context->name] = $context; + } + } + } + return $contexts; + } +} diff --git a/plugins/context_condition_context_all.inc b/plugins/context_condition_context_all.inc new file mode 100644 index 0000000..faecbb3 --- /dev/null +++ b/plugins/context_condition_context_all.inc @@ -0,0 +1,29 @@ +condition_used()) { + $active_contexts = array_keys(context_active_contexts()); + foreach ($this->get_contexts() as $context) { + + // Only test contexts that haven't been activated yet, + // and have values set. + if (!in_array($context->name, $active_contexts, TRUE) && $values = $this->fetch_from_context($context, 'values')) { + + // The condition is met if all contexts are active. + if (count(array_intersect($values, $active_contexts)) == count($values)) { + $this->condition_met($context); + } + } + } + + // If the list of active contexts has changed, we need to recurse. + if ($active_contexts != array_keys(context_active_contexts())) { + $this->execute(); + } + } + } +} diff --git a/plugins/context_condition_default.inc b/plugins/context_condition_default.inc new file mode 100644 index 0000000..b463c0a --- /dev/null +++ b/plugins/context_condition_default.inc @@ -0,0 +1,35 @@ + t('Default context')); + } + + function editor_form($context = NULL) { + $form = parent::editor_form($context); + $form[1]['#title'] = t('Default context'); + $form['#weight'] = -10; + return $form; + } + + function execute() { + if ($this->condition_used()) { + $active_contexts = context_active_contexts(); + + foreach ($active_contexts as $name => $context) { + foreach (array_keys($context->conditions) as $cond) { + if (!in_array($cond, array('default', 'sitewide'))) { + return; + } + } + } + foreach ($this->get_contexts('context_condition_default') as $context) { + $this->condition_met($context, 'context_condition_default'); + } + } + } +} diff --git a/plugins/context_condition_language.inc b/plugins/context_condition_language.inc new file mode 100644 index 0000000..2d9cc43 --- /dev/null +++ b/plugins/context_condition_language.inc @@ -0,0 +1,16 @@ +get_contexts($value) as $context) { + $this->condition_met($context, $value); + } + } +} diff --git a/plugins/context_condition_menu.inc b/plugins/context_condition_menu.inc new file mode 100644 index 0000000..0df8811 --- /dev/null +++ b/plugins/context_condition_menu.inc @@ -0,0 +1,80 @@ + 0)); + $root_menus = array(); + foreach ($menus as $key => $name) { + $id = explode(':', $key); + if ($id[1] == '0') { + $root_menus[$id[0]] = check_plain($name); + } + else { + $link = menu_link_load($id[1]); + $identifier = $link['link_path']; + $root_menu = $root_menus[$id[0]]; + while (isset($menus[$root_menu][$identifier])) { + $identifier .= "'"; + } + $menus[$root_menu][$identifier] = $name; + } + unset($menus[$key]); + } + array_unshift($menus, "-- " . t('None') . " --"); + } + else { + $menus = array(); + } + return $menus; + } + + /** + * Override of condition_form(). + * Use multiselect widget. + */ + function condition_form($context) { + $form = parent::condition_form($context); + $menu_count = count($form['#options'], COUNT_RECURSIVE); + $form['#type'] = 'select'; + $form['#multiple'] = TRUE; + $form['#attributes'] = array('size' => $menu_count > 20 ? 20 : $menu_count); + return $form; + } + + /** + * Override of condition_form_submit(). + * Trim any identifier padding for non-unique path menu items. + */ + function condition_form_submit($values) { + // Trim any identifier padding for non-unique path menu items. + $values = parent::condition_form_submit($values); + $trimmed = array(); + foreach ($values as $key => $value) { + $trimmed[trim($key, "'")] = trim($value, "'"); + } + return $trimmed; + } + + /** + * Override of execute(). + */ + function execute() { + if ($this->condition_used()) { + $trail = menu_get_active_trail(); + foreach ($trail as $item) { + if (!empty($item['href'])) { + foreach ($this->get_contexts($item['href']) as $context) { + $this->condition_met($context, $item['href']); + } + } + } + } + } +} diff --git a/plugins/context_condition_node.inc b/plugins/context_condition_node.inc new file mode 100644 index 0000000..6c2c534 --- /dev/null +++ b/plugins/context_condition_node.inc @@ -0,0 +1,62 @@ +type] = check_plain($type->name); + } + return $values; + } + + function options_form($context) { + $defaults = $this->fetch_from_context($context, 'options'); + return array( + 'node_form' => array( + '#title' => t('Set on node form'), + '#type' => 'select', + '#options' => array( + CONTEXT_NODE_VIEW => t('No'), + CONTEXT_NODE_FORM => t('Yes'), + CONTEXT_NODE_FORM_ONLY => t('Only on node form') + ), + '#description' => t('Set this context on node forms'), + '#default_value' => isset($defaults['node_form']) ? $defaults['node_form'] : TRUE, + ), + ); + } + + function execute($node, $op) { + foreach ($this->get_contexts($node->type) as $context) { + // Check the node form option. + $options = $this->fetch_from_context($context, 'options'); + if ($op === 'form') { + $options = $this->fetch_from_context($context, 'options'); + if (!empty($options['node_form']) && in_array($options['node_form'], array(CONTEXT_NODE_FORM, CONTEXT_NODE_FORM_ONLY))) { + $this->condition_met($context, $node->type); + } + } + elseif (empty($options['node_form']) || $options['node_form'] != CONTEXT_NODE_FORM_ONLY) { + $this->condition_met($context, $node->type); + } + } + } +} diff --git a/plugins/context_condition_node_taxonomy.inc b/plugins/context_condition_node_taxonomy.inc new file mode 100644 index 0000000..cfb214f --- /dev/null +++ b/plugins/context_condition_node_taxonomy.inc @@ -0,0 +1,73 @@ +tags)) { + foreach (taxonomy_get_tree($vocab->vid) as $term) { + $values[$term->tid] = check_plain($term->name); + } + } + } + } + return $values; + } + + function condition_form($context) { + $form = parent::condition_form($context); + $form['#type'] = 'select'; + $form['#size'] = 12; + $form['#multiple'] = TRUE; + $vocabularies = taxonomy_get_vocabularies(); + $options = array(); + foreach ($vocabularies as $vid => $vocabulary) { + $tree = taxonomy_get_tree($vid); + if ($tree && (count($tree) > 0)) { + $options[$vocabulary->name] = array(); + foreach ($tree as $term) { + $options[$vocabulary->name][$term->tid] = str_repeat('-', $term->depth) . $term->name; + } + } + } + $form['#options'] = $options; + return $form; + } + + function execute($node, $op) { + // build a list of each taxonomy reference field belonging to the bundle for the current node + $fields = field_info_fields(); + $instance_fields = field_info_instances('node', $node->type); + $check_fields = array(); + foreach ($instance_fields as $key => $field_info) { + if ($fields[$key]['type'] == 'taxonomy_term_reference') { + $check_fields[] = $key; + } + } + + if ($this->condition_used() && !empty($check_fields)) { + foreach ($check_fields as $field) { + if ($terms = field_get_items('node', $node, $field)) { + foreach ($terms as $term) { + foreach ($this->get_contexts($term['tid']) as $context) { + // Check the node form option. + if ($op === 'form') { + $options = $this->fetch_from_context($context, 'options'); + if (!empty($options['node_form'])) { + $this->condition_met($context, $term['tid']); + } + } + else { + $this->condition_met($context, $term['tid']); + } + } + } + } + } + } + } +} diff --git a/plugins/context_condition_path.inc b/plugins/context_condition_path.inc new file mode 100644 index 0000000..d3717c8 --- /dev/null +++ b/plugins/context_condition_path.inc @@ -0,0 +1,116 @@ +fetch_from_context($context, 'values')); + return $form; + } + + /** + * Condition form submit handler. + */ + function condition_form_submit($values) { + $parsed = array(); + $items = explode("\n", $values); + if (!empty($items)) { + foreach ($items as $v) { + $v = trim($v); + if (!empty($v)) { + $parsed[$v] = $v; + } + } + } + return $parsed; + } + + /** + * Execute. + */ + function execute() { + if ($this->condition_used()) { + // Include both the path alias and normal path for matching. + $current_path = array(drupal_get_path_alias($_GET['q'])); + if ($current_path[0] != $_GET['q']) { + $current_path[] = $_GET['q']; + } + foreach ($this->get_contexts() as $context) { + $paths = $this->fetch_from_context($context, 'values'); + if ($this->match($current_path, $paths, TRUE)) { + $this->condition_met($context); + } + } + } + } + + /** + * Match the subject against a set of regex patterns. + * Similar to drupal_match_path() but also handles negation through the use + * of the ~ character. + * + * @param mixed $subject + * The subject string or an array of strings to be matched. + * @param array $patterns + * An array of patterns. Any patterns that begin with ~ are considered + * negative or excluded conditions. + * @param boolean $path + * Whether the given subject should be matched as a Drupal path. If TRUE, + * '' will be replaced with the site frontpage when matching against + * $patterns. + */ + protected function match($subject, $patterns, $path = FALSE) { + static $regexps; + $match = FALSE; + $positives = $negatives = 0; + $subject = !is_array($subject) ? array($subject) : $subject; + foreach ($patterns as $pattern) { + if (strpos($pattern, '~') === 0) { + $negate = TRUE; + $negatives++; + } + else { + $negate = FALSE; + $positives++; + } + $pattern = ltrim($pattern, '~'); + if (!isset($regexps[$pattern])) { + if ($path) { + $regexps[$pattern] = '/^(' . preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/', '/(^|\|)\\\\($|\|)/'), array('|', '.*', '\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'), preg_quote($pattern, '/')) . ')$/'; + } + else { + $regexps[$pattern] = '/^(' . preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/'), array('|', '.*'), preg_quote($pattern, '/')) . ')$/'; + } + } + foreach ($subject as $value) { + if (preg_match($regexps[$pattern], $value)) { + if ($negate) { + return FALSE; + } + $match = TRUE; + } + } + } + // If there are **only** negative conditions and we've gotten this far none + // we actually have a match. + if ($positives === 0 && $negatives) { + return TRUE; + } + return $match; + } +} diff --git a/plugins/context_condition_query_string.inc b/plugins/context_condition_query_string.inc new file mode 100644 index 0000000..781d4cb --- /dev/null +++ b/plugins/context_condition_query_string.inc @@ -0,0 +1,28 @@ +condition_used()) { + $current_query_string = $_SERVER["QUERY_STRING"]; + foreach ($this->get_contexts() as $context) { + $query_strings = $this->fetch_from_context($context, 'values'); + if ($this->match($current_query_string, $query_strings, TRUE)) { + $this->condition_met($context); + } + } + } + } +} diff --git a/plugins/context_condition_sitewide.inc b/plugins/context_condition_sitewide.inc new file mode 100644 index 0000000..11e64c1 --- /dev/null +++ b/plugins/context_condition_sitewide.inc @@ -0,0 +1,23 @@ + t('Always active')); + } + + function editor_form($context = NULL) { + $form = parent::editor_form($context); + $form[1]['#title'] = t('Always active'); + $form['#weight'] = -10; + return $form; + } + + function execute($value) { + foreach ($this->get_contexts($value) as $context) { + $this->condition_met($context, $value); + } + } +} diff --git a/plugins/context_condition_taxonomy_term.inc b/plugins/context_condition_taxonomy_term.inc new file mode 100644 index 0000000..e3e0478 --- /dev/null +++ b/plugins/context_condition_taxonomy_term.inc @@ -0,0 +1,47 @@ +machine_name] = check_plain($vocab->name); + } + return $values; + } + + function options_form($context) { + $defaults = $this->fetch_from_context($context, 'options'); + return array( + 'term_form' => array( + '#title' => t('Set on term form'), + '#type' => 'select', + '#options' => array( + 0 => t('No'), + 1 => t('Yes'), + 2 => t('Only on term form') + ), + '#description' => t('Set this context on term forms'), + '#default_value' => isset($defaults['term_form']) ? $defaults['term_form'] : TRUE, + ), + ); + } + + function execute($term, $op) { + foreach ($this->get_contexts($term->vocabulary_machine_name) as $context) { + // Check the node form option. + $options = $this->fetch_from_context($context, 'options'); + if ($op === 'form') { + $options = $this->fetch_from_context($context, 'options'); + if (!empty($options['term_form']) && in_array($options['term_form'], array(1, 2))) { + $this->condition_met($context, $term->vocabulary_machine_name); + } + } + elseif (empty($options['term_form']) || $options['term_form'] != 2) { + $this->condition_met($context, $term->vocabulary_machine_name); + } + } + } +} diff --git a/plugins/context_condition_user.inc b/plugins/context_condition_user.inc new file mode 100644 index 0000000..f7184f0 --- /dev/null +++ b/plugins/context_condition_user.inc @@ -0,0 +1,31 @@ + $role_name) { + if ($rid == DRUPAL_ANONYMOUS_RID) { + $values['anonymous user'] = check_plain($role_name); + } + elseif ($rid == DRUPAL_AUTHENTICATED_RID) { + $values['authenticated user'] = check_plain($role_name); + } + else { + $values[$role_name] = check_plain($role_name); + } + } + return $values; + } + + function execute($account) { + $roles = $account->roles; + foreach ($roles as $rid => $role) { + foreach ($this->get_contexts($role) as $context) { + $this->condition_met($context, $role); + } + } + } +} diff --git a/plugins/context_condition_user_page.inc b/plugins/context_condition_user_page.inc new file mode 100644 index 0000000..4e1596b --- /dev/null +++ b/plugins/context_condition_user_page.inc @@ -0,0 +1,59 @@ +fetch_from_context($context, 'options'); + return array( + 'mode' => array( + '#title' => t('Active for'), + '#type' => 'select', + '#options' => array( + 'all' => t('Any user'), + 'current' => t('Only the current user'), + 'other' => t('Only other users'), + ), + '#default_value' => isset($defaults['mode']) ? $defaults['mode'] : 'all', + ), + ); + } + + function execute($account, $op) { + global $user; + foreach ($this->get_contexts($op) as $context) { + if ($op === 'register') { + $this->condition_met($context); + } + else { + $options = $this->fetch_from_context($context, 'options'); + $mode = !empty($options['mode']) ? $options['mode'] : 'all'; + switch ($options['mode']) { + case 'current': + if ($account->uid == $user->uid) { + $this->condition_met($context); + } + break; + case 'other': + if ($account->uid != $user->uid) { + $this->condition_met($context); + } + break; + case 'all': + default: + $this->condition_met($context); + break; + } + } + } + } +} diff --git a/plugins/context_condition_views.inc b/plugins/context_condition_views.inc new file mode 100644 index 0000000..4cb92f6 --- /dev/null +++ b/plugins/context_condition_views.inc @@ -0,0 +1,48 @@ +name]->disabled) || !$views[$view->name]->disabled) { + $enabled_views[$view->name] = check_plain($view->name); + + // Provide more granular options for each page display + $displays = array(); + foreach ($view->display as $id => $display) { + if ($display->display_plugin == 'page') { + $displays[$view->name . ":" . $id] = check_plain("-- {$display->display_title}"); + } + } + $enabled_views += $displays; + } + } + return $enabled_views; + } + + function execute($view) { + switch ($view->display_handler->display->display_plugin) { + case 'page': + case 'calendar': + // Set contexts for this view. + foreach ($this->get_contexts($view->name) as $context) { + $this->condition_met($context, $view->name); + } + // Set any contexts associated with the current display + if (!empty($view->current_display)) { + foreach ($this->get_contexts("{$view->name}:{$view->current_display}") as $context) { + $this->condition_met($context, "{$view->name}:{$view->current_display}"); + } + } + break; + } + } +} diff --git a/plugins/context_reaction.inc b/plugins/context_reaction.inc new file mode 100644 index 0000000..f324339 --- /dev/null +++ b/plugins/context_reaction.inc @@ -0,0 +1,86 @@ + $val) { + if (is_object($val) || (is_array($val))) { + $this->{$key} = unserialize(serialize($val)); + } + } + } + + /** + * Constructor. Do not override. + */ + function __construct($plugin, $info) { + $this->plugin = $plugin; + $this->title = isset($info['title']) ? $info['title'] : $plugin; + $this->description = isset($info['description']) ? $info['description'] : ''; + } + + function options_form($context) { + return array(); + } + + /** + * Options form submit handler. + */ + function options_form_submit($values) { + return $values; + } + + /** + * Settings form. Provide variable settings for your reaction. + */ + function settings_form() { + return array(); + } + + /** + * Public method that is called from hooks or other integration points with + * Drupal. Note that it is not implemented in the base class, allowing + * extending classes to change the function signature if necessary. + * + * function execute($value) { + * foreach ($this->get_contexts($value) as $context) { + * $this->condition_met($context, $value); + * } + * } + */ + + /** + * Retrieve active contexts that have values for this reaction. + */ + function get_contexts() { + $contexts = array(); + foreach (context_active_contexts() as $context) { + if ($this->fetch_from_context($context)) { + $contexts[$context->name] = $context; + } + } + return $contexts; + } + + /** + * Retrieve options from the context provided. + */ + function fetch_from_context($context) { + return isset($context->reactions[$this->plugin]) ? $context->reactions[$this->plugin] : array(); + } +} diff --git a/plugins/context_reaction_block.css b/plugins/context_reaction_block.css new file mode 100644 index 0000000..e82a6a6 --- /dev/null +++ b/plugins/context_reaction_block.css @@ -0,0 +1,262 @@ +/** + * Script placeholder markup. + */ +.script-placeholder { + padding:100px 0px; + text-align:center; + } + +/** + * Browser + */ +.context-block-browser { + width: 600px; +} + +.context-block-browser .blocks { + height:98%; + overflow: auto; + float: left; + width: 320px; +} + +.context-block-browser .block-browser-sidebar { + float: left; + width: 250px; + padding: 0 0 0 15px; +} + +.context-block-item, +.context-block-browser .draggable-placeholder, +#admin-toolbar .context-block-browser .context-block-item { + font-size:11px; + line-height:20px; + height:20px; + + color:#333; + + padding:3px 3px 3px 3px; + margin:0px 1px 1px 0px; + max-width:300px; + white-space:nowrap; + overflow:hidden; + + background:#efefef; + border:1px solid #ddd; + position:relative; + + border-radius:5px; + -moz-border-radius:5px; + -moz-user-select:none; + -webkit-user-select:none; +} + + + .context-block-addable { cursor: pointer; } + + .context-block-item span.icon { + background:url(context_reaction_block.png) 0px -80px no-repeat; + display:block; + width:20px; + height:20px; + float:left; + margin-right:5px; + } + + .context-block-loading { max-width:none; } + + .context-block-loading span.icon { + background-position:-20px -80px; + float:none; + margin:0px auto; + } + + .context-block-browser .draggable-placeholder { padding:2px 1px 1px 2px; } + + #admin-toolbar.horizontal .context-block-browser .draggable-placeholder, + #admin-toolbar.horizontal .context-block-browser .context-block-item { + width:180px; + margin-right:1px; + padding-right:9px; + float:left; + } + + + .context-block-added { display:none !important; } + +/** + * Inline editing elements ============================================ + */ +div.context-block-region {display: none;} +a.context-block { display:none !important; } + +body.context-editing div.context-block-region { + -moz-border-radius:5px; + -webkit-border-radius:5px; + background:#666; + color:#fff; + opacity: 0.5; + -moz-opacity: 0.5; + filter:alpha(opacity=50); + + display:block; + height:40px; + line-height:24px; + + text-align:center; + font-size:18px; + white-space:nowrap; +} + +.context-block-region .region-name { + width:100%; + text-align:center; + font-size:18px; + color:#fff; + white-space:nowrap; + display:block; + -moz-user-select:none; + -webkit-user-select:none; + } + +body.context-editing .ui-sortable .block { opacity:.25; } + +body.context-editing .ui-sortable .draggable { + position:relative; + opacity:1; + } + +body.context-editing .draggable-placeholder { + -moz-border-radius:5px; + -webkit-border-radius:5px; + + background:#fff; + border:3px dashed #666; + opacity:.2; + } + +body.context-editing .draggable:hover a.context-block-remove, +body.context-editing .draggable:hover a.context-block-handle { + background:url(context_reaction_block.png) no-repeat; + cursor:move; + display:block; + width:40px; + height:40px; + position:absolute; + right:35px; + top:-5px; + z-index:100; + } + +body.context-editing .draggable:hover a.context-block-remove { + background-position:-40px 0px; + cursor:pointer; + right:-5px; + } + + .context-block-hidden { display:none !important; } + + .block .context-block-empty-content { + text-align:center; + padding:10px; + opacity:.5; + background:#fff; + color:#666; + } + +/** + * Block visibility =================================================== + */ +#context-blockform .context-blockform-selector { + overflow:auto; + } + +#context-blockform span.system-blocks { color:#999; } + +#context-blockform td.blocks, +#context-blockform td.selector { + border:1px solid #ddd; + padding:10px; + width:50%; + vertical-align: top; + } + +#context-blockform td.blocks .label, +#context-blockform td.blocks td, +#context-blockform td.blocks th { + background:#fff; + padding:5px 5px 4px; + border:0px; + border-bottom:1px solid #ddd; + } + + #context-blockform td.blocks .label { background:#eee; } + #context-blockform td.blocks .label a { float:right; } + +#context-ui-items #context-blockform { + font-size:11px; + line-height:15px; + } + +#context-ui-items #context-blockform .form-checkboxes { + height:auto; + overflow:visible; + padding:0px; + margin:0px; + border:0px; + } + +#context-ui-items #context-blockform .form-item { padding:0px; } + +#context-ui-items #context-blockform label { + background:#eee; + display:block; + padding:5px; + line-height:15px; + } + +#context-ui-items #context-blockform label.option { + background:#fff; + display:block; + border:0px; + } + +#context-blockform .tabledrag-toggle-weight-wrapper { + margin-bottom:0; + } + +a.context-ui-add-link, a:link.context-ui-add-link, a:visited.context-ui-add-link { + display:block; + width:100%; + text-align:center; + font-size:12px; + color:#fff; + cursor: pointer; + line-height:14px; +} + +.editing-context-label { + position: fixed; + top:70px; + background:#fff; + color:#222; + padding:10px; + font-weight:bold; + opacity: 0.5; + -moz-opacity: 0.5; + filter:alpha(opacity=50); + border:1px solid #ddd; + border-left:0; + border-radius:0 6px 6px 0; +} + +.context-help { + font-size:12px; + font-weight:normal; +} + +.context-editor-title { + font-size:24px; + margin:10px 0px; + padding:0; +} diff --git a/plugins/context_reaction_block.inc b/plugins/context_reaction_block.inc new file mode 100644 index 0000000..abcafd8 --- /dev/null +++ b/plugins/context_reaction_block.inc @@ -0,0 +1,681 @@ +get_blocks(NULL, NULL, $this->rebuild_needed()); + $this->rebuild_needed(FALSE); + + $theme_key = variable_get('theme_default', 'garland'); + $weight_delta = $this->max_block_weight(); + + $form = array( + '#tree' => TRUE, + '#theme' => 'context_block_form', + 'max_block_weight' => array( + '#value' => $weight_delta, + '#type' => 'value', + ), + 'state' => array( + '#type' => 'hidden', + '#attributes' => array('class' => 'context-blockform-state'), + ), + ); + + /** + * Selector. + */ + $modules = module_list(); + $form['selector'] = array( + '#type' => 'item', + '#tree' => TRUE, + '#prefix' => '
', + '#suffix' => '
', + ); + foreach ($this->get_blocks() as $block) { + $group = isset($block->context_group) ? $block->context_group : $block->module; + if (!isset($form['selector'][$group])) { + $form['selector'][$group] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => isset($block->context_group) ? $block->context_group : $modules[$block->module], + ); + $form['selector'][$group]['checkboxes'] = array( + '#type' => 'checkboxes', + '#options' => array(), + ); + } + $form['selector'][$group]['checkboxes']['#options'][$block->bid] = check_plain($block->info); + } + + ksort($form['selector']); + + /** + * Regions. + */ + $form['blocks'] = array( + '#tree' => TRUE, + '#theme' => 'context_block_regions_form', + ); + foreach ($this->system_region_list($theme_key, REGIONS_VISIBLE) as $region => $label) { + $form['blocks'][$region] = array( + '#type' => 'item', + '#title' => $label, + '#tree' => TRUE, + ); + foreach ($this->get_blocks($region, $context) as $block) { + if (!empty($block->context)) { + $form['blocks'][$region][$block->bid] = array( + '#value' => check_plain($block->info), + '#weight' => $block->weight, + '#type' => 'markup', + '#tree' => TRUE, + 'weight' => array('#type' => 'weight', '#delta' => $weight_delta, '#default_value' => $block->weight), + ); + } + } + } + return $form; + } + + /** + * Options form submit handler. + */ + function options_form_submit($values) { + $blocks = array(); + $block_info = $this->get_blocks(); + + // Retrieve blocks from submitted JSON string. + if (!empty($values['state'])) { + $edited = $this->json_decode($values['state']); + } + else { + $edited = array(); + } + + foreach ($edited as $region => $block_data) { + foreach ($block_data as $position => $data) { + if (isset($block_info[$data->bid])) { + $blocks[$data->bid] = array( + 'module' => $block_info[$data->bid]->module, + 'delta' => $block_info[$data->bid]->delta, + 'region' => $region, + 'weight' => $data->weight, + ); + } + } + } + return array('blocks' => $blocks); + } + + /** + * Context editor form for blocks. + */ + function editor_form($context) { + $form = array(); + drupal_add_library('system', 'ui.droppable'); + drupal_add_library('system', 'ui.sortable'); + drupal_add_js(drupal_get_path('module', 'context_ui') . '/json2.js'); + drupal_add_js(drupal_get_path('module', 'context_ui') . '/theme/filter.js'); + drupal_add_js(drupal_get_path('module', 'context') . '/plugins/context_reaction_block.js'); + drupal_add_css(drupal_get_path('module', 'context') . '/plugins/context_reaction_block.css'); + + // We might be called multiple times so use a static to ensure this is set just once. + static $once; + if (!isset($once)) { + $settings = array( + 'path' => drupal_is_front_page() ? base_path() : url($_GET['q']), + 'params' => (object) array_diff_key($_GET, array('q' => '')), + 'scriptPlaceholder' => theme('context_block_script_placeholder', array('text' => '')), + ); + drupal_add_js(array('contextBlockEditor' => $settings), 'setting'); + $once = TRUE; + } + + $form['state'] = array( + '#type' => 'hidden', + '#attributes' => array('class' => array('context-block-editor-state')), + ); + $form['browser'] = array( + '#markup' => theme('context_block_browser', array( + 'blocks' => $this->get_blocks(NULL, NULL, $this->rebuild_needed()), + 'context' => $context + )), + ); + $this->rebuild_needed(FALSE); + return $form; + } + + /** + * Submit handler context editor form. + */ + function editor_form_submit(&$context, $values) { + $edited = !empty($values['state']) ? (array) $this->json_decode($values['state']) : array(); + + $options = array(); + + // Take the existing context values and remove blocks that belong affected regions. + $affected_regions = array_keys($edited); + if (!empty($context->reactions['block']['blocks'])) { + $options = $context->reactions['block']; + foreach ($options['blocks'] as $key => $block) { + if (in_array($block['region'], $affected_regions)) { + unset($options['blocks'][$key]); + } + } + } + + // Iterate through blocks and add in the ones that belong to the context. + foreach ($edited as $region => $blocks) { + foreach ($blocks as $weight => $block) { + if ($block->context === $context->name) { + $split = explode('-', $block->bid); + $options['blocks'][$block->bid] = array( + 'module' => array_shift($split), + 'delta' => implode('-', $split), + 'region' => $region, + 'weight' => $weight, + ); + } + } + } + + return $options; + } + + /** + * Settings form for variables. + */ + function settings_form() { + $form = array(); + $form['context_reaction_block_all_regions'] = array( + '#title' => t('Show all regions'), + '#type' => 'checkbox', + '#default_value' => variable_get('context_reaction_block_all_regions', FALSE), + '#description' => t('Show all regions including those that are empty. Enable if you are administering your site using the inline editor.') + ); + return $form; + } + + /** + * Execute. + */ + function execute(&$page) { + global $theme; + + // The theme system might not yet be initialized. We need $theme. + drupal_theme_initialize(); + + // If the context_block querystring param is set, switch to AJAX rendering. + // Note that we check the output buffer for any content to ensure that we + // are not in the middle of a PHP template render. + if (isset($_GET['context_block']) && !ob_get_contents()) { + return $this->render_ajax($_GET['context_block']); + } + + // Populate all block regions + $all_regions = $this->system_region_list($theme); + + // Load all region content assigned via blocks. + foreach (array_keys($all_regions) as $region) { + if ($this->is_enabled_region($region)) { + if ($blocks = $this->block_get_blocks_by_region($region)) { + + // Are the blocks already sorted. + $blocks_sorted = TRUE; + + // If blocks have already been placed in this region (most likely by + // Block module), then merge in blocks from Context. + if (isset($page[$region])) { + $page[$region] = array_merge($page[$region], $blocks); + + // Restore the weights that Block module manufactured + // @see _block_get_renderable_array() + foreach ($page[$region] as &$block) { + if (isset($block['#block']->weight)) { + $block['#weight'] = $block['#block']->weight; + $blocks_sorted = FALSE; + } + } + } + else { + $page[$region] = $blocks; + } + + $page[$region]['#sorted'] = $blocks_sorted; + } + } + } + } + + /** + * Return a list of enabled regions for which blocks should be built. + * Split out into a separate method for easy overrides in extending classes. + */ + protected function is_enabled_region($region) { + global $theme; + $regions = array_keys($this->system_region_list($theme)); + return in_array($region, $regions, TRUE); + } + + /** + * Determine whether inline editing requirements are met and that the current + * user may edit. + */ + protected function is_editable_region($region, $reset = FALSE) { + // Check requirements. + // Generally speaking, it does not make sense to allow anonymous users to + // edit a context inline. Though it may be possible to save (and restore) + // an edited context to an anonymous user's cookie or session, it's an + // edge case and probably not something we want to encourage anyway. + static $requirements; + if (!isset($requirements) || $reset) { + global $user; + if ($user->uid && user_access('administer contexts') && variable_get('context_ui_dialog_enabled', FALSE)) { + $requirements = TRUE; + drupal_add_library('system', 'ui.droppable'); + drupal_add_library('system', 'ui.sortable'); + drupal_add_js(drupal_get_path('module', 'context') . '/plugins/context_reaction_block.js'); + drupal_add_css(drupal_get_path('module', 'context') . '/plugins/context_reaction_block.css'); + } + else { + $requirements = FALSE; + } + } + // Check that this region is not locked by the theme. + global $theme; + $info = system_get_info('theme', $theme); + if ($info && isset($info['regions_locked']) && in_array($region, $info['regions_locked'])) { + return FALSE; + } + // Check that this region is not hidden + $visible = $this->system_region_list($theme, REGIONS_VISIBLE); + return $requirements && $this->is_enabled_region($region) && isset($visible[$region]); + } + + /** + * Add markup for making a block editable. + */ + protected function editable_block($block) { + if (!empty($block->content)) { + $block->content['#theme_wrappers'][] = 'context_block_edit_wrap'; + } + else { + // the block alter in context.module should ensure that blocks are never + // empty if the inline editor is present but in the case that they are, + // warn that editing the context is likely to cause this block to be dropped + drupal_set_message(t('The block with delta @delta from module @module is not compatible with the inline editor and will be dropped from the context containing it if you edit contexts here', array('@delta' => $block->delta, '@module' => $block->module)), 'warning'); + } + return $block; + } + + /** + * Add markup for making a region editable. + */ + protected function editable_region($region, $build) { + if ($this->is_editable_region($region) && + (!empty($build) || + variable_get('context_reaction_block_all_regions', FALSE) || + context_isset('context_ui', 'context_ui_editor_present')) + ) { + global $theme; + $regions = $this->system_region_list($theme); + $name = isset($regions[$region]) ? $regions[$region] : $region; + // The negative weight + sorted will push our region marker to the top of the region + $build['context'] = array( + '#prefix' => "
", + '#markup' => "{$name}" . + "" . t('Add a block here.') . '', + '#suffix' => '
', + '#weight' => -100, + ); + $build['#sorted'] = FALSE; + } + + return $build; + } + + /** + * Get a renderable array of a region containing all enabled blocks. + */ + function block_get_blocks_by_region($region) { + module_load_include('module', 'block', 'block'); + + $build = array(); + if ($list = $this->block_list($region)) { + $build = _block_get_renderable_array($list); + } + if ($this->is_editable_region($region)) { + $build = $this->editable_region($region, $build); + } + return $build; + } + + /** + * An alternative version of block_list() that provides any context enabled blocks. + */ + function block_list($region) { + module_load_include('module', 'block', 'block'); + + $context_blocks = &drupal_static('context_reaction_block_list');; + $contexts = context_active_contexts(); + if (!isset($context_blocks)) { + $info = $this->get_blocks(); + $context_blocks = array(); + foreach ($contexts as $context) { + $options = $this->fetch_from_context($context); + if (!empty($options['blocks'])) { + foreach ($options['blocks'] as $context_block) { + $bid = "{$context_block['module']}-{$context_block['delta']}"; + if (isset($info[$bid])) { + $block = (object) array_merge((array) $info[$bid], $context_block); + $block->context = $context->name; + $block->title = isset($info[$block->bid]->title) ? $info[$block->bid]->title : NULL; + $block->cache = isset($info[$block->bid]->cache) ? $info[$block->bid]->cache : DRUPAL_NO_CACHE; + $context_blocks[$block->region][$block->bid] = $block; + } + } + } + } + + $this->is_editable_check($context_blocks); + global $theme; + $active_regions = $this->system_region_list($theme); + + // Make context renders regions in the same order as core. + $_context_blocks = array(); + foreach ($active_regions as $r => $name) { + if (isset($context_blocks[$r])) { + $_context_blocks[$r] = $context_blocks[$r]; + } + } + $context_blocks = $_context_blocks; + unset($_context_blocks); + + foreach ($context_blocks as $r => $blocks) { + //only render blocks in an active region + if (array_key_exists($r, $active_regions)) { + $context_blocks[$r] = _block_render_blocks($blocks); + + // Make blocks editable if allowed. + if ($this->is_editable_region($r)) { + foreach ($context_blocks[$r] as $key => $block) { + $context_blocks[$r][$key] = $this->editable_block($block); + } + } + } + + // Sort blocks. + uasort($context_blocks[$r], array('context_reaction_block', 'block_sort')); + } + } + return isset($context_blocks[$region]) ? $context_blocks[$region] : array(); + } + + /** + * Determine if there is an active context editor block, and set a flag. We will set a flag so + * that we can make sure that blocks with empty content have some default content. This is + * needed so the save of the context inline editor does not remove the blocks with no content. + */ + function is_editable_check($context_blocks) { + foreach ($context_blocks as $r => $blocks) { + if (isset($blocks['context_ui-editor'])) { + $block = $blocks['context_ui-editor']; + // see if the editor is actually enabled, lifted from _block_render_blocks + if (!count(module_implements('node_grants')) && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD') && ($cid = _block_get_cache_id($block)) && ($cache = cache_get($cid, 'cache_block'))) { + $array = $cache->data; + } + else { + $array = module_invoke($block->module, 'block_view', $block->delta); + drupal_alter(array('block_view', "block_view_{$block->module}_{$block->delta}"), $array, $block); + } + if(!empty($array['content'])) { + context_set('context_ui', 'context_ui_editor_present', TRUE); + } + break; + } + } + } + + /** + * Generate the safe weight range for a block being added to a region such that + * there are enough potential unique weights to support all blocks. + */ + protected function max_block_weight() { + $blocks = $this->get_blocks(); + $block_count = 0; + foreach ($blocks as $region => $block_list) { + $block_count += count($block_list); + } + // Add 2 to make sure there's space at either end of the block list + return round(($block_count + 2) / 2); + } + + /** + * Check or set whether a rebuild of the block info cache is needed. + */ + function rebuild_needed($set = NULL) { + if (isset($set) && $set != variable_get('context_block_rebuild_needed', FALSE)) { + variable_set('context_block_rebuild_needed', $set); + } + return (bool) variable_get('context_block_rebuild_needed', FALSE); + } + + /** + * Helper function to generate a list of blocks from a specified region. If provided a context object, + * will generate a full list of blocks for that region distinguishing between system blocks and + * context-provided blocks. + * + * @param $region + * The string identifier for a theme region. e.g. "left" + * @param $context + * A context object. + * + * @return + * A keyed (by "module_delta" convention) array of blocks. + */ + function get_blocks($region = NULL, $context = NULL, $reset = FALSE) { + static $block_info; + $theme_key = variable_get('theme_default', 'garland'); + + if (!isset($block_info) || $reset) { + $block_info = array(); + if (!$reset) { + $block_info = context_cache_get('block_info'); + } + if (empty($block_info)) { + if (module_exists('block')) { + $block_blocks = _block_rehash($theme_key); + $block_info = array(); + // Change from numeric keys to module-delta. + foreach ($block_blocks as $block) { + $block = (object) $block; + unset($block->theme, $block->status, $block->weight, $block->region, $block->custom, $block->visibility, $block->pages); + $block->bid = "{$block->module}-{$block->delta}"; + $block_info[$block->bid] = $block; + } + } + else { + $block_info = array(); + foreach (module_implements('block_info') as $module) { + $module_blocks = module_invoke($module, 'block_info'); + if (!empty($module_blocks)) { + foreach ($module_blocks as $delta => $block) { + $block = (object) $block; + $block->module = $module; + $block->delta = $delta; + $block->bid = "{$block->module}-{$block->delta}"; + $block_info[$block->bid] = $block; + } + } + } + } + context_cache_set('block_info', $block_info); + } + // Allow other modules that may declare blocks dynamically to alter + // this list. + drupal_alter('context_block_info', $block_info); + + // Gather only region info from the database. + if (module_exists('block')) { + $result = db_select('block') + ->fields('block') + ->condition('theme', $theme_key) + ->execute() + ->fetchAllAssoc('bid'); + + drupal_alter('block_list', $result); + drupal_alter('context_block_list', $result); + + foreach ($result as $row) { + if (isset($block_info["{$row->module}-{$row->delta}"])) { + $block_info["{$row->module}-{$row->delta}"] = (object) array_merge((array) $row, (array) $block_info["{$row->module}-{$row->delta}"]); + unset($block_info["{$row->module}-{$row->delta}"]->status); + unset($block_info["{$row->module}-{$row->delta}"]->visibility); + } + } + } + } + + $blocks = array(); + + // No region specified, provide all blocks. + if (!isset($region)) { + $blocks = $block_info; + } + // Region specified. + else { + foreach ($block_info as $bid => $block) { + if (isset($block->region) && $block->region == $region) { + $blocks[$bid] = $block; + } + } + } + + // Add context blocks if provided. + if (is_object($context) && $options = $this->fetch_from_context($context)) { + if (!empty($options['blocks'])) { + foreach ($options['blocks'] as $block) { + if ( + isset($block_info["{$block['module']}-{$block['delta']}"]) && // Block is valid. + (!isset($region) || (!empty($region) && $block['region'] == $region)) // No region specified or regions match. + ) { + $context_block = $block_info["{$block['module']}-{$block['delta']}"]; + $context_block->weight = $block['weight']; + $context_block->region = $block['region']; + $context_block->context = !empty($context->name) ? $context->name : 'tempname'; + $blocks[$context_block->bid] = $context_block; + } + } + } + uasort($blocks, array('context_reaction_block', 'block_sort')); + } + return $blocks; + } + + /** + * Sort callback. + */ + static function block_sort($a, $b) { + return ($a->weight - $b->weight); + } + + /** + * Compatibility wrapper around json_decode(). + */ + protected function json_decode($json, $assoc = FALSE) { + // Requires PHP 5.2. + if (function_exists('json_decode')) { + return json_decode($json, $assoc); + } + else { + watchdog('context', 'Please upgrade your PHP version to one that supports json_decode.'); + } + } + + /** + * Block renderer for AJAX requests. Triggered when $_GET['context_block'] + * is set. See ->execute() for how this is called. + */ + function render_ajax($param) { + // Besure the page isn't a 404 or 403. + $headers = drupal_get_http_header(); + if (array_key_exists('status', $headers) && ($headers['status'] == "404 Not Found" || $headers['status'] == "403 Forbidden")) { + return; + } + // Set the header right away. This will inform any players in the stack + // that we are in the middle of responding to an AJAX request. + drupal_add_http_header('Content-Type', 'text/javascript; charset=utf-8'); + + if (strpos($param, ',') !== FALSE) { + list($bid, $context) = explode(',', $param); + list($module, $delta) = explode('-', $bid, 2); + // Check token to make sure user has access to block. + if (!(user_access('administer contexts') || user_access('context ajax block access') || $this->context_block_ajax_rendering_allowed($bid))) { + echo drupal_json_encode(array('status' => 0)); + exit; + } + + // Ensure $bid is valid. + $info = $this->get_blocks(); + if (isset($info[$bid])) { + module_load_include('module', 'block', 'block'); + $block = $info[$bid]; + $block->title = isset($block->title) ? $block->title : ''; + $block->context = $context; + $block->region = ''; + $rendered_blocks = _block_render_blocks(array($block)); // For E_STRICT warning + $block = array_shift($rendered_blocks); + if (empty($block->content['#markup'])) { + $block->content['#markup'] = "
" . t('This block appears empty when displayed on this page.') . "
"; + } + $block = $this->editable_block($block); + $renderable_block = _block_get_renderable_array(array($block)); // For E_STRICT warning + echo drupal_json_encode(array( + 'status' => 1, + 'block' => drupal_render($renderable_block), + )); + drupal_exit(); + } + } + echo drupal_json_encode(array('status' => 0)); + drupal_exit(); + } + + /** + * Provide caching for system_region_list since it can get called + * frequently. Evaluate for removal once https://drupal.org/node/1873450 + * lands or system_region_list is otherwise cached in core + */ + protected function system_region_list($theme_key, $show = REGIONS_ALL) { + static $cache = array(); + if (!isset($cache[$theme_key])) { + $cache[$theme_key] = array(); + } + if (!isset($cache[$theme_key][$show])) { + $cache[$theme_key][$show] = system_region_list($theme_key, $show); + } + return $cache[$theme_key][$show]; + } + + /** + * Allow modules to selectively allow ajax rendering of a specific block + */ + private function context_block_ajax_rendering_allowed($bid) { + $allowed = FALSE; + foreach (module_invoke_all('context_allow_ajax_block_access', $bid) as $module_allow) { + $allowed = $allow || $module_allow; + if ($allowed) { + break; + } + } + return $allowed; + } +} diff --git a/plugins/context_reaction_block.js b/plugins/context_reaction_block.js new file mode 100644 index 0000000..10e25a2 --- /dev/null +++ b/plugins/context_reaction_block.js @@ -0,0 +1,496 @@ +(function($){ +Drupal.behaviors.contextReactionBlock = {attach: function(context) { + $('form.context-editor:not(.context-block-processed)') + .addClass('context-block-processed') + .each(function() { + var id = $(this).attr('id'); + Drupal.contextBlockEditor = Drupal.contextBlockEditor || {}; + $(this).bind('init.pageEditor', function(event) { + Drupal.contextBlockEditor[id] = new DrupalContextBlockEditor($(this)); + }); + $(this).bind('start.pageEditor', function(event, context) { + // Fallback to first context if param is empty. + if (!context) { + context = $(this).data('defaultContext'); + } + Drupal.contextBlockEditor[id].editStart($(this), context); + }); + $(this).bind('end.pageEditor', function(event) { + Drupal.contextBlockEditor[id].editFinish(); + }); + }); + + // + // Admin Form ======================================================= + // + // ContextBlockForm: Init. + $('#context-blockform:not(.processed)').each(function() { + $(this).addClass('processed'); + Drupal.contextBlockForm = new DrupalContextBlockForm($(this)); + Drupal.contextBlockForm.setState(); + }); + + // ContextBlockForm: Attach block removal handlers. + // Lives in behaviors as it may be required for attachment to new DOM elements. + $('#context-blockform a.remove:not(.processed)').each(function() { + $(this).addClass('processed'); + $(this).click(function() { + $(this).parents('tr').eq(0).remove(); + Drupal.contextBlockForm.setState(); + return false; + }); + }); + + // Conceal Section title, subtitle and class + $('div.context-block-browser', context).nextAll('.form-item').hide(); +}}; + +/** + * Context block form. Default form for editing context block reactions. + */ +DrupalContextBlockForm = function(blockForm) { + this.state = {}; + + this.setState = function() { + $('table.context-blockform-region', blockForm).each(function() { + var region = $(this).attr('id').split('context-blockform-region-')[1]; + var blocks = []; + $('tr', $(this)).each(function() { + var bid = $(this).attr('id'); + var weight = $(this).find('select,input').first().val(); + blocks.push({'bid' : bid, 'weight' : weight}); + }); + Drupal.contextBlockForm.state[region] = blocks; + }); + + // Serialize here and set form element value. + $('form input.context-blockform-state').val(JSON.stringify(this.state)); + + // Hide enabled blocks from selector that are used + $('table.context-blockform-region tr').each(function() { + var bid = $(this).attr('id'); + $('div.context-blockform-selector input[value='+bid+']').parents('div.form-item').eq(0).hide(); + }); + // Show blocks in selector that are unused + $('div.context-blockform-selector input').each(function() { + var bid = $(this).val(); + if ($('table.context-blockform-region tr#'+bid).size() === 0) { + $(this).parents('div.form-item').eq(0).show(); + } + }); + + }; + + // make sure we update the state right before submits, this takes care of an + // apparent race condition between saving the state and the weights getting set + // by tabledrag + $('#ctools-export-ui-edit-item-form').submit(function() { Drupal.contextBlockForm.setState(); }); + + // Tabledrag + // Add additional handlers to update our blocks. + $.each(Drupal.settings.tableDrag, function(base) { + var table = $('#' + base + ':not(.processed)', blockForm); + if (table && table.is('.context-blockform-region')) { + table.addClass('processed'); + table.bind('mouseup', function(event) { + Drupal.contextBlockForm.setState(); + return; + }); + } + }); + + // Add blocks to a region + $('td.blocks a', blockForm).each(function() { + $(this).click(function() { + var region = $(this).attr('href').split('#')[1]; + var base = "context-blockform-region-"+ region; + var selected = $("div.context-blockform-selector input:checked"); + if (selected.size() > 0) { + var weight_warn = false; + var min_weight_option = -10; + var max_weight_option = 10; + var max_observed_weight = min_weight_option - 1; + $('table#' + base + ' tr').each(function() { + var weight_input_val = $(this).find('select,input').first().val(); + if (+weight_input_val > +max_observed_weight) { + max_observed_weight = weight_input_val; + } + }); + + selected.each(function() { + // create new block markup + var block = document.createElement('tr'); + var text = $(this).parents('div.form-item').eq(0).hide().children('label').text(); + var select = '
'; + $(block).attr('id', $(this).attr('value')).addClass('draggable'); + $(block).html(""+ text + "" + select + "X"); + + // add block item to region + //TODO : Fix it so long blocks don't get stuck when added to top regions and dragged towards bottom regions + Drupal.tableDrag[base].makeDraggable(block); + $('table#'+base).append(block); + if ($.cookie('Drupal.tableDrag.showWeight') == 1) { + $('table#'+base).find('.tabledrag-hide').css('display', ''); + $('table#'+base).find('.tabledrag-handle').css('display', 'none'); + } + else { + $('table#'+base).find('.tabledrag-hide').css('display', 'none'); + $('table#'+base).find('.tabledrag-handle').css('display', ''); + } + Drupal.attachBehaviors($('table#'+base)); + + Drupal.contextBlockForm.setState(); + $(this).removeAttr('checked'); + }); + if (weight_warn) { + alert(Drupal.t('Desired block weight exceeds available weight options, please check weights for blocks before saving')); + } + } + return false; + }); + }); +}; + +/** + * Context block editor. AHAH editor for live block reaction editing. + */ +DrupalContextBlockEditor = function(editor) { + this.editor = editor; + this.state = {}; + this.blocks = {}; + this.regions = {}; + + return this; +}; + +DrupalContextBlockEditor.prototype = { + initBlocks : function(blocks) { + var self = this; + this.blocks = blocks; + blocks.each(function() { + if($(this).hasClass('context-block-empty')) { + $(this).removeClass('context-block-hidden'); + } + $(this).addClass('draggable'); + $(this).prepend($('')); + $(this).prepend($('').click(function() { + $(this).parent ('.block').eq(0).fadeOut('medium', function() { + $(this).remove(); + self.updateBlocks(); + }); + return false; + })); + }); + }, + initRegions : function(regions) { + this.regions = regions; + var ref = this; + + $(regions).not('.context-ui-processed') + .each(function(index, el) { + $('.context-ui-add-link', el).click(function(e){ + ref.showBlockBrowser($(this).parent()); + }).addClass('context-ui-processed'); + }); + $('.context-block-browser').hide(); + }, + showBlockBrowser : function(region) { + var toggled = false; + //figure out the id of the context + var activeId = $('.context-editing', this.editor).attr('id').replace('-trigger', ''), + context = $('#' + activeId)[0]; + + this.browser = $('.context-block-browser', context).addClass('active'); + + //add the filter element to the block browser + if (!this.browser.has('input.filter').size()) { + var parent = $('.block-browser-sidebar .filter', this.browser); + var list = $('.blocks', this.browser); + new Drupal.Filter (list, false, '.context-block-addable', parent); + } + //show a dialog for the blocks list + this.browser.show().dialog({ + modal : true, + close : function() { + $(this).dialog('destroy'); + //reshow all the categories + $('.category', this).show(); + $(this).hide().appendTo(context).removeClass('active'); + }, + height: (.8 * $(window).height()), + minHeight:400, + minWidth:680, + width:680 + }); + + //handle showing / hiding block items when a different category is selected + $('.context-block-browser-categories', this.browser).change(function(e) { + //if no category is selected we want to show all the items + if ($(this).val() == 0) { + $('.category', self.browser).show(); + } else { + $('.category', self.browser).hide(); + $('.category-' + $(this).val(), self.browser).show(); + } + }); + + //if we already have the function for a different context, rebind it so we don't get dupes + if(this.addToRegion) { + $('.context-block-addable', this.browser).unbind('click.addToRegion') + } + + //protected function for adding a clicked block to a region + var self = this; + this.addToRegion = function(e){ + var ui = { + 'item' : $(this).clone(), + 'sender' : $(region) + }; + $(this).parents('.context-block-browser.active').dialog('close'); + $(region).after(ui.item); + self.addBlock(e, ui, this.editor, activeId.replace('context-editable-', '')); + }; + + $('.context-block-addable', this.browser).bind('click.addToRegion', this.addToRegion); + }, + // Update UI to match the current block states. + updateBlocks : function() { + var browser = $('div.context-block-browser'); + + // For all enabled blocks, mark corresponding addables as having been added. + $('.block, .admin-block').each(function() { + var bid = $(this).attr('id').split('block-')[1]; // Ugh. + }); + // For all hidden addables with no corresponding blocks, mark as addable. + $('.context-block-item', browser).each(function() { + var bid = $(this).attr('id').split('context-block-addable-')[1]; + }); + + // Mark empty regions. + $(this.regions).each(function() { + if ($('.block:has(a.context-block)', this).size() > 0) { + $(this).removeClass('context-block-region-empty'); + } + else { + $(this).addClass('context-block-region-empty'); + } + }); + }, + // Live update a region + updateRegion : function(event, ui, region, op) { + switch (op) { + case 'over': + $(region).removeClass('context-block-region-empty'); + break; + case 'out': + if ( + // jQuery UI 1.8 + $('.draggable-placeholder', region).size() === 1 && + $('.block:has(a.context-block)', region).size() == 0 + ) { + $(region).addClass('context-block-region-empty'); + } + break; + } + }, + // Remove script elements while dragging & dropping. + scriptFix : function(event, ui, editor, context) { + if ($('script', ui.item)) { + var placeholder = $(Drupal.settings.contextBlockEditor.scriptPlaceholder); + var label = $('div.handle label', ui.item).text(); + placeholder.children('strong').html(label); + $('script', ui.item).parent().empty().append(placeholder); + } + }, + // Add a block to a region through an AJAX load of the block contents. + addBlock : function(event, ui, editor, context) { + var self = this; + if (ui.item.is('.context-block-addable')) { + var bid = ui.item.attr('id').split('context-block-addable-')[1]; + + // Construct query params for our AJAX block request. + var params = Drupal.settings.contextBlockEditor.params; + params.context_block = bid + ',' + context; + + // Replace item with loading block. + //ui.sender.append(ui.item); + + var blockLoading = $('
'); + ui.item.addClass('context-block-added'); + ui.item.after(blockLoading); + + + $.getJSON(Drupal.settings.contextBlockEditor.path, params, function(data) { + if (data.status) { + var newBlock = $(data.block); + if ($('script', newBlock)) { + $('script', newBlock).remove(); + } + blockLoading.fadeOut(function() { + $(this).replaceWith(newBlock); + self.initBlocks(newBlock); + self.updateBlocks(); + Drupal.attachBehaviors(newBlock); + }); + } + else { + blockLoading.fadeOut(function() { $(this).remove(); }); + } + }); + } + else if (ui.item.is(':has(a.context-block)')) { + self.updateBlocks(); + } + }, + // Update form hidden field with JSON representation of current block visibility states. + setState : function() { + var self = this; + + $(this.regions).each(function() { + var region = $('.context-block-region', this).attr('id').split('context-block-region-')[1]; + var blocks = []; + $('a.context-block', $(this)).each(function() { + if ($(this).attr('class').indexOf('edit-') != -1) { + var bid = $(this).attr('id').split('context-block-')[1]; + var context = $(this).attr('class').split('edit-')[1].split(' ')[0]; + context = context ? context : 0; + var block = {'bid': bid, 'context': context}; + blocks.push(block); + } + }); + self.state[region] = blocks; + }); + // Serialize here and set form element value. + $('input.context-block-editor-state', this.editor).val(JSON.stringify(this.state)); + }, + //Disable text selection. + disableTextSelect : function() { + if ($.browser.safari) { + $('.block:has(a.context-block):not(:has(input,textarea))').css('WebkitUserSelect','none'); + } + else if ($.browser.mozilla) { + $('.block:has(a.context-block):not(:has(input,textarea))').css('MozUserSelect','none'); + } + else if ($.browser.msie) { + $('.block:has(a.context-block):not(:has(input,textarea))').bind('selectstart.contextBlockEditor', function() { return false; }); + } + else { + $(this).bind('mousedown.contextBlockEditor', function() { return false; }); + } + }, + //Enable text selection. + enableTextSelect : function() { + if ($.browser.safari) { + $('*').css('WebkitUserSelect',''); + } + else if ($.browser.mozilla) { + $('*').css('MozUserSelect',''); + } + else if ($.browser.msie) { + $('*').unbind('selectstart.contextBlockEditor'); + } + else { + $(this).unbind('mousedown.contextBlockEditor'); + } + }, + // Start editing. Attach handlers, begin draggable/sortables. + editStart : function(editor, context) { + var self = this; + // This is redundant to the start handler found in context_ui.js. + // However it's necessary that we trigger this class addition before + // we call .sortable() as the empty regions need to be visible. + $(document.body).addClass('context-editing'); + this.editor.addClass('context-editing'); + this.disableTextSelect(); + this.initBlocks($('.block:has(a.context-block.edit-'+context+')')); + this.initRegions($('.context-block-region').parent()); + this.updateBlocks(); + + $('a.context_ui_dialog-stop').hide(); + + $('.editing-context-label').remove(); + var label = $('#context-editable-trigger-'+context+' .label').text(); + label = Drupal.t('Now Editing: ') + label; + editor.parent().parent() + .prepend('
'+ label + '
'); + + // First pass, enable sortables on all regions. + $(this.regions).each(function() { + var region = $(this); + var params = { + revert: true, + dropOnEmpty: true, + placeholder: 'draggable-placeholder', + forcePlaceholderSize: true, + items: '> *:has(a.context-block.editable)', + handle: 'a.context-block-handle', + start: function(event, ui) { self.scriptFix(event, ui, editor, context); }, + stop: function(event, ui) { self.addBlock(event, ui, editor, context); }, + receive: function(event, ui) { self.addBlock(event, ui, editor, context); }, + over: function(event, ui) { self.updateRegion(event, ui, region, 'over'); }, + out: function(event, ui) { self.updateRegion(event, ui, region, 'out'); }, + cursorAt: {left: 300, top: 0} + }; + region.sortable(params); + }); + + // Second pass, hook up all regions via connectWith to each other. + $(this.regions).each(function() { + $(this).sortable('option', 'connectWith', ['.ui-sortable']); + }); + + // Terrible, terrible workaround for parentoffset issue in Safari. + // The proper fix for this issue has been committed to jQuery UI, but was + // not included in the 1.6 release. Therefore, we do a browser agent hack + // to ensure that Safari users are covered by the offset fix found here: + // http://dev.jqueryui.com/changeset/2073. + if ($.ui.version === '1.6' && $.browser.safari) { + $.browser.mozilla = true; + } + }, + // Finish editing. Remove handlers. + editFinish : function() { + this.editor.removeClass('context-editing'); + this.enableTextSelect(); + + $('.editing-context-label').remove(); + + // Remove UI elements. + $(this.blocks).each(function() { + $('a.context-block-handle, a.context-block-remove', this).remove(); + if($(this).hasClass('context-block-empty')) { + $(this).addClass('context-block-hidden'); + } + $(this).removeClass('draggable'); + }); + + $('a.context_ui_dialog-stop').show(); + + this.regions.sortable('destroy'); + + this.setState(); + + // Unhack the user agent. + if ($.ui.version === '1.6' && $.browser.safari) { + $.browser.mozilla = false; + } + } +}; //End of DrupalContextBlockEditor prototype + +})(jQuery); diff --git a/plugins/context_reaction_block.png b/plugins/context_reaction_block.png new file mode 100644 index 0000000000000000000000000000000000000000..50ad859c4125cdaa04bcc35330de748a8119c08c GIT binary patch literal 2677 zcmcgu_dna~7yks;s=ZpoxT>Tz(n)PqGetCns2DYZ7*RD_RE-)nL)E7Cjf>h!NqTEE z9by(GqKMXB)qL%5?!WN;;d@^1^PJatUe9=)=bZO>JxNxUCcNAd+yDUZnwc6RnAVD^ zVVvyD(PJa{H`A~^G%&N_WJ(05dm_{43NW>Q$Xq}1uYwF91!2rd@gO5qkTnhy6na0< z0|*TbRrU7wdFXaOz(W-m=vlC#Ey3KlV`g~ECaiG15N9s=Lu!CXDY7V1tv9>U=bv^k zM8N-LV=$bJy(3t~B6rouyWB95M?_h;O)gVC+x9njW|oTi{B&= z;ug=V3?&SBNAaqD={(iXa-&(dGuC&T&jyU$?4M+G9C??dyWij6r|xPqzK1fj!Rayv z7mcnWkx02{Rsd$_1O-9DVva9b4Ma<8n-(3l4Rw)%fxrT2U2JM=Yg2nuS7$NkQ8X>}zW>j>?XM0vvPl~F0RU8r6crW0 z>Q`n57v)(U931HH6Zo}U8_zMpC!@RO<>&X};a&AJm6erx-yl^T9l755*}_lHs-kl* zDJdNykjNTBa#K?i6pHS+{Nj1%-pI&^T!~ibMzxBHN?>PhT3VXjXrHg4CEK(2_p_#a zoyOSmzby3y+u+vjTk*@*hcO60=)cGyzO30O1e!}q{9?sYTtKym_~RG=4KI^EOqZgygETM(D8 zzkiN(WPLECz^z9~Q&UqqBs4UkMR)w`SIzu_0{YSt=1p5$2-1mJXEo*J3$JEFn#3l@ z#^mkn?6eRsDuq#wj_Y1nEWX(f&R+)UzhK546cWO&BTXUnuB>29f`fxym*O?Gv|{n{ ze;D3PS@nK?vuJSGJXTUtlAfQRpS^R{!`NEWi7HY%II3wRQa4 zCaF}v(ri~svCb};P9SIk70QP{%hPvsq~TWh ziqew}j7eluYLI=Nqc46>IjavugTy`1me8`bIv8$_{W2j|nQ@!hCF=@>Q@% zArQ76b-yYuE-qz8$$SPK9i3YZ6J45`b~ZLQebuIyEGj01GcD&ZWLAJklMx4l3$2l& z$H&L7W@l%|*CvEY+uPMAX*8L8$k`=9S_c9E}!dV z#zx%^1D8Y{EVoRo^5&W=I1US#$TfrAG^B2d=A`y1uVZXg{ z$CEX1M|*3zy2NQ^W#t|kg}MwPF{Mo2?9Q7S2PiYunC7XGwEyJw{!0ohmb0*+%VKAL`k8yq?Fon@-yJkXJ}uJYiH z$?o+TP(YRytd;KfgvA+^cTo_ij?|@90D#gbTfLcYy6X_#v77^iK~`wp(c+zm3D4l9 zhTkA(_hOCZvw4dD5SIYi(FTphdGF8sc@~Ep6o#m3hT#7fnc2=Q-`)8r`oBL&C{3L^;@8jNX5;ckK@!EzNdY*Oi$h<_Gv8!IeQ4U8 zj=8}g#mC1>en1!ZTQpA{VLPJ-(q&!_ENttsLhKdgIZ1wM1qsc*k3ZR;4~U9#nn=W# zl)%3#6%-X6Cqa@=*i)YPdF{ z>Q|QE+cp}zTs}8;VF=q=uaqI+3YM1Mcy1`SL(Wn>l}Iv-0X9IFS;B^0F0XZltlvBS z1SO)mWzVG=ONDgl{9P^M@J{0C=;){j&_6IBm{dXgeF+NXr_<>`bXi$h5Q_?c28vnc zDzhyWvXg))-`0DQ#>U1`e(lqKaT(1N9mdS2_@KA7{k24|!xU8&l~+tQum5}Cq?g{f zzN{?V!ou-rt(h`ds2ThbezC|x`E;oOmyASqbCWWMPQr<_C(@(}NU~+Ymn!#ASiz@! z@07OF5?Ri~fdy~p7}*Lc)atH?hENidEccd!G!uv?2R;u2Q>gwW%4|qU(A! zj}N$6N?6$xS`ZTxlkPd(=%U&+ICT?8r5ozVq;iw=li~G{)3TZxFPs7g91cJ1>g=pX z_eAX5t*YrKf=JLa+}toqp~mitQo~yW0);}A60q)A?8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/context_reaction_breadcrumb.inc b/plugins/context_reaction_breadcrumb.inc new file mode 100644 index 0000000..66d121c --- /dev/null +++ b/plugins/context_reaction_breadcrumb.inc @@ -0,0 +1,39 @@ +fields('menu_links', array('p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8')) + ->condition('hidden', 0) + ->condition('link_path', $item['link_path']) + ->execute(); + while ($parents = $result->fetchAssoc()) { + $set = FALSE; + foreach (array_filter($parents) as $plid) { + $parent = menu_link_load($plid); + if ($parent && $parent['access'] && empty($parent['hidden']) && !empty($parent['title'])) { + $set = TRUE; + $breadcrumb[] = l($parent['title'], $parent['href']); + } + } + // Only set the breadcrumb if one or more links were added to the + // trail. If not, continue iterating through possible menu links. + if ($set) { + drupal_set_breadcrumb($breadcrumb); + break; + } + } + } + /** + * Return the title to be used for the current menu item. + */ + function get_link_title($item) { + return module_exists('i18n_menu') ? _i18n_menu_link_title($item) : $item['title']; + } +} diff --git a/plugins/context_reaction_css_injector.inc b/plugins/context_reaction_css_injector.inc new file mode 100644 index 0000000..0ff790f --- /dev/null +++ b/plugins/context_reaction_css_injector.inc @@ -0,0 +1,33 @@ + $this->title, + '#description' => $this->description, + '#options' => $list, + '#type' => 'checkboxes', + '#default_value' => $this->fetch_from_context($context), + ); + } + + function execute() { + $contexts = $this->get_contexts(); + foreach ($contexts as $context) { + if (!empty($context->reactions[$this->plugin])) { + foreach ($context->reactions[$this->plugin] as $crid => $enabled) { + if ($enabled && $css_rule = _css_injector_load_rule($crid)) { + drupal_add_css(_css_injector_rule_uri($crid), 'module', $css_rule['media'], $css_rule['preprocess']); + } + } + } + } + } +} diff --git a/plugins/context_reaction_debug.inc b/plugins/context_reaction_debug.inc new file mode 100644 index 0000000..97f3867 --- /dev/null +++ b/plugins/context_reaction_debug.inc @@ -0,0 +1,46 @@ + array('#type' => 'value', '#value' => TRUE)); + } + + function options_form_submit($values) { + return array('debug' => 1); + } + + /** + * Settings form for variables. + */ + function settings_form() { + $form = array(); + $form['context_reaction_debug_enable_global'] = array( + '#title' => t('Enable debug reaction on all contexts'), + '#type' => 'checkbox', + '#default_value' => variable_get('context_reaction_debug_enable_global', FALSE), + '#description' => t('Enable the debug reaction on all contexts.') + ); + return $form; + } + + /** + * Output a list of active contexts. + */ + function execute() { + $contexts = context_active_contexts(); + foreach ($contexts as $context) { + if (!empty($context->reactions['debug']) || variable_get('context_reaction_debug_enable_global', FALSE)) { + if (user_access('administer site configuration') && module_exists('context_ui')) { + $name = l($context->name, "admin/structure/context/list/{$context->name}", array('query' => array('destination' => $_GET['q']))); + } + else { + $name = check_plain($context->name); + } + drupal_set_message(t("Active context: !name", array('!name' => $name))); + } + } + } +} diff --git a/plugins/context_reaction_menu.inc b/plugins/context_reaction_menu.inc new file mode 100644 index 0000000..0d82adc --- /dev/null +++ b/plugins/context_reaction_menu.inc @@ -0,0 +1,143 @@ + 0)); + $menu_names = array(); + foreach ($menus as $id => $title) { + list($menu_name, $mlid) = explode(':', $id); + // Store the title each menu for reference. + if ($mlid == '0') { + $menu_names[$menu_name] = $title; + } + else { + $link = menu_link_load($mlid); + $identifier = $link['link_path']; + $root_menu = $menu_names[$menu_name]; + while (isset($options[$root_menu][$identifier])) { + $identifier .= "'"; + } + $options[$root_menu][$menu_name . ':' . $identifier] = $title; + } + } + } + return array( + '#title' => $this->title, + '#description' => $this->description, + '#options' => $options, + '#type' => 'select', + '#multiple' => TRUE, + '#default_value' => $this->fetch_from_context($context), + ); + } + + /** + * Override of options_form_submit(). + * Trim any identifier padding for non-unique path menu items. + */ + function options_form_submit($values) { + $trimmed = array(); + foreach ($values as $value) { + $value = trim($value, "'"); + $trimmed[] = $value; + } + return $trimmed; + } + + /** + * Overrides parent function to include legacy handling for old format of just storing a single path. + */ + function fetch_from_context($context) { + $values = parent::fetch_from_context($context); + // Legacy - convert single string value to an array with a preferred menu + if (is_string($values)) { + $menu = menu_link_get_preferred($values); + if (!$menu) { + return array(); + } + return array($menu['menu_name'] . ':' . $menu['link_path']); + } + return $values; + } + + /** + * Provide active trail in all menus in which our path appears. + */ + function execute(&$vars = NULL) { + $menu_names = menu_get_active_menu_names(); + $active_paths = $this->get_active_paths(); + foreach ($menu_names as $menu_name) { + if (isset($active_paths[$menu_name])) { + foreach($active_paths[$menu_name] as $path) { + if ($link = menu_link_get_preferred($path, $menu_name)) { + $this->set_active_trail_from_link($link); + return; + } + } + } + } + + // None of the links can be found in their preferred menus. Instead we just try to find any of the paths in any + // menu. Note that the preferred menu names list is still used but not always honoured. + // We hope to not have to fall into this section as we could end up doing rather a lot of lookups. + foreach ($active_paths as $menu_name => $paths) { + foreach($paths as $path) { + if ($link = menu_link_get_preferred($path)) { + $this->set_active_trail_from_link($link); + return; + } + } + } + } + + /** + * Helper function to build and set the active trail from a menu link. + * + * @param $item + * A menu link item. + */ + function set_active_trail_from_link($item) { + menu_tree_set_path($item['menu_name'], $item['link_path']); + $trail = array(); + while($item) { + array_unshift($trail, $item); + $item = menu_link_load($item['plid']); + } + array_unshift($trail, array( + 'title' => t('Home'), + 'href' => '', + 'link_path' => '', + 'localized_options' => array(), + 'type' => 0, + )); + menu_set_active_trail($trail); + } + + /** + * Helper function to return the list of currently active paths. + * + * The paths are grouped by menu name. + */ + function get_active_paths() { + $active_paths = array(); + foreach ($this->get_contexts() as $context) { + $paths = $this->fetch_from_context($context); + $active_paths = array_merge($active_paths, $paths); + } + + $by_menu_name = array(); + foreach ($active_paths as $id) { + list($menu_name, $path) = explode(':', $id); + $by_menu_name[$menu_name][] = $path; + } + return $by_menu_name; + } +} diff --git a/plugins/context_reaction_region.inc b/plugins/context_reaction_region.inc new file mode 100644 index 0000000..c4f0fd7 --- /dev/null +++ b/plugins/context_reaction_region.inc @@ -0,0 +1,46 @@ +fetch_from_context($context); + $form = array(); + foreach (list_themes() as $theme) { + if ($theme->status) { + $regions = system_region_list($theme->name); + $default = isset($values[$theme->name]) ? $values[$theme->name]['disable'] : array(); + + $form[$theme->name] = array( + '#type' => 'fieldset', + '#title' => "Disable Regions in {$theme->name} Theme", + '#collapsible' => TRUE, + '#collapsed' => !array_reduce($default, create_function('$a, $b', 'return $a || $b;')), + ); + $form[$theme->name]['disable'] = array( + '#type' => 'checkboxes', + '#title' => t("Disable the following"), + '#options' => $regions, + '#default_value' => $default, + ); + } + } + return $form; + } + + function execute(&$page) { + global $theme; + foreach ($this->get_contexts() as $k => $v) { + if (isset($v->reactions[$this->plugin][$theme])) { + $regions = $v->reactions[$this->plugin][$theme]['disable']; + foreach ($regions as $region => $disable) { + if ($disable && isset($page[$region])) { + unset($page[$region]); + } + } + } + } + } +} diff --git a/plugins/context_reaction_template_suggestions.inc b/plugins/context_reaction_template_suggestions.inc new file mode 100644 index 0000000..0376f0a --- /dev/null +++ b/plugins/context_reaction_template_suggestions.inc @@ -0,0 +1,47 @@ +fetch_from_context($context); + + return array( + '#title' => t('Template suggestions'), + '#type' => 'textarea', + '#description' => t('Enter template suggestions such as "page__front", one per line, in order of preference (using underscores instead of hyphens). For more information, please visit ') . l(t('Drupal 7 Template (Theme Hook) Suggestions'), 'http://drupal.org/node/1089656', array(array('target' => '_blank'), 'html' => TRUE,)) . '.', + '#default_value' => is_string($default_value) ? $default_value : '', + ); + } + + /** + * Add any new template suggestions to the current list. + */ + function execute(&$vars = NULL) { + + // Get the list of contexts associated with this reaction. + $contexts = $this->get_contexts(); + + // Iterate through each, and process those with something set. + foreach ($contexts as $context) { + if (isset($context->reactions) && (!empty($context->reactions[$this->plugin]))) { + + // Get the suggestion data entered by the user. + $suggestions = $this->fetch_from_context($context, 'values'); + + // Convert it to an list and reverse it (as higher priority items + // should be on the bottom). + $suggestions = array_reverse(explode("\n", $suggestions)); + + // Append the suggested list to the existing list. + $vars['theme_hook_suggestions'] = array_merge($vars['theme_hook_suggestions'], $suggestions); + } + } + } +} + diff --git a/plugins/context_reaction_theme.inc b/plugins/context_reaction_theme.inc new file mode 100644 index 0000000..4d5b677 --- /dev/null +++ b/plugins/context_reaction_theme.inc @@ -0,0 +1,67 @@ +options_form($context); + + // Hide descriptions which take up too much space. + unset($form['title']['#description']); + unset($form['subtitle']['#description']); + unset($form['class']['#description']); + return $form; + } + + /** + * Submit handler for editor form. + */ + function editor_form_submit($context, $values) { + return $values; + } + + /** + * Allow admins to provide a section title, section subtitle and section class. + */ + function options_form($context) { + $values = $this->fetch_from_context($context); + $form = array( + '#tree' => TRUE, + '#title' => t('Theme variables'), + 'title' => array( + '#title' => t('Section title'), + '#description' => t('Provides this text as a $section_title variable for display in page.tpl.php when this context is active.'), + '#type' => 'textfield', + '#maxlength' => 255, + '#default_value' => isset($values['title']) ? $values['title'] : '', + ), + 'subtitle' => array( + '#title' => t('Section subtitle'), + '#description' => t('Provides this text as a $section_subtitle variable for display in page.tpl.php when this context is active.'), + '#type' => 'textfield', + '#maxlength' => 255, + '#default_value' => isset($values['subtitle']) ? $values['subtitle'] : '', + ), + ); + return $form; + } + + /** + * Set 'section_title', and 'section_subtitle' if not set + */ + function execute(&$vars) { + $classes = array(); + foreach ($this->get_contexts() as $k => $v) { + if (!empty($v->reactions[$this->plugin]['title']) && !isset($vars['section_title'])) { + $vars['section_title'] = check_plain(t($v->reactions[$this->plugin]['title'])); + } + if (!empty($v->reactions[$this->plugin]['subtitle']) && !isset($vars['section_subtitle'])) { + $vars['section_subtitle'] = check_plain(t($v->reactions[$this->plugin]['subtitle'])); + } + } + } +} diff --git a/plugins/context_reaction_theme_html.inc b/plugins/context_reaction_theme_html.inc new file mode 100644 index 0000000..42d1722 --- /dev/null +++ b/plugins/context_reaction_theme_html.inc @@ -0,0 +1,34 @@ +fetch_from_context($context); + $form = array( + 'class' => array( + '#title' => t('Section class'), + '#description' => t('Provides this text as an additional body class (in $classes in html.tpl.php) when this section is active.'), + '#type' => 'textfield', + '#maxlength' => 64, + '#default_value' => isset($values['class']) ? $values['class'] : '', + ), + ); + return $form; + } + + /** + * Set additional classes onto the 'body_classes'. + */ + function execute(&$vars) { + $classes = array(); + foreach ($this->get_contexts() as $k => $v) { + if (!empty($v->reactions[$this->plugin]['class'])) { + $vars['classes_array'][] = $v->reactions[$this->plugin]['class']; + } + } + } +} diff --git a/tests/context.conditions.test b/tests/context.conditions.test new file mode 100644 index 0000000..e2cee73 --- /dev/null +++ b/tests/context.conditions.test @@ -0,0 +1,561 @@ + 'Condition: user', + 'description' => 'Test user condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools'); + $this->user1 = $this->drupalCreateUser(array('access content', 'administer site configuration')); + $this->user2 = $this->drupalCreateUser(array('access content')); + + // The role name is not reliably put on the user object. Retrive from + // user_roles(). + $role = ''; + foreach (array_keys($this->user1->roles) as $rid) { + if ($rid !== DRUPAL_AUTHENTICATED_RID) { + $role = user_role_load($rid)->name; + break; + } + } + + // Create test context. + ctools_include('export'); + $this->context = ctools_export_new_object('context'); + $this->context->name = 'testcontext'; + $this->context->conditions = array('user' => array('values' => array($role))); + $this->context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($this->context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + } + + function tearDown() { + parent::tearDown(); + context_delete($this->context); + user_delete($this->user1->uid); + user_delete($this->user2->uid); + } + + function test() { + // User 1 triggers the context. + $this->drupalLogin($this->user1); + $this->drupalGet('node'); + $this->assertText('Active context: testcontext'); + + // User 2 does not. + $this->drupalLogin($this->user2); + $this->drupalGet('node'); + $this->assertNoText('Active context: testcontext'); + } +} + +class ContextConditionUserPageTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: user page', + 'description' => 'Test user page condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools'); + $this->user1 = $this->drupalCreateUser(array('access user profiles', 'access content', 'administer site configuration')); + $this->user2 = $this->drupalCreateUser(array('access user profiles', 'access content')); + + // Create test context. + ctools_include('export'); + $this->context = ctools_export_new_object('context'); + $this->context->name = 'testcontext'; + $this->context->conditions = array('user_page' => array('values' => array('view' => 'view'), 'options' => array('mode' => 'all'))); + $this->context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($this->context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + } + + function tearDown() { + parent::tearDown(); + context_delete($this->context); + $edit = array(); + user_delete($this->user1->uid); + user_delete($this->user2->uid); + } + + function test() { + // Viewing any user profile triggers context. + $this->drupalLogin($this->user1); + $this->drupalGet("user/{$this->user1->uid}"); + $this->assertText('Active context: testcontext'); + $this->drupalGet("user/{$this->user2->uid}"); + $this->assertText('Active context: testcontext'); + // User form does not. + $this->drupalGet("user/{$this->user1->uid}/edit"); + $this->assertNoText('Active context: testcontext'); + + // Test current user mode + $this->context->conditions['user_page']['options']['mode'] = 'current'; + $saved = context_save($this->context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + $this->drupalGet("user/{$this->user1->uid}"); + $this->assertText('Active context: testcontext'); + $this->drupalGet("user/{$this->user2->uid}"); + $this->assertNoText('Active context: testcontext'); + + // Test other user mode + $this->context->conditions['user_page']['options']['mode'] = 'other'; + $saved = context_save($this->context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + $this->drupalGet("user/{$this->user1->uid}"); + $this->assertNoText('Active context: testcontext'); + $this->drupalGet("user/{$this->user2->uid}"); + $this->assertText('Active context: testcontext'); + } +} + +class ContextConditionNodeTaxonomyTest extends DrupalWebTestCase { + // We want the default taxonomy and content types created + protected $profile = 'standard'; + + public static function getInfo() { + return array( + 'name' => 'Condition: taxonomy', + 'description' => 'Test taxonomy condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'taxonomy'); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'create article content')); + $this->drupalLogin($admin_user); + + // Create test terms. + $this->vocab = taxonomy_vocabulary_machine_name_load('tags'); + + $this->terms = array(); + $this->terms['apples'] = (object)array('name' => 'apples', 'vid' => $this->vocab->vid); + $this->terms['oranges'] = (object)array('name' => 'oranges', 'vid' => $this->vocab->vid); + taxonomy_term_save($this->terms['apples']); + taxonomy_term_save($this->terms['oranges']); + + // Create test context. + ctools_include('export'); + $this->context = ctools_export_new_object('context'); + $this->context->name = 'testcontext'; + $this->context->conditions = array('node_taxonomy' => array('values' => array($this->terms['apples']->tid))); + $this->context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($this->context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + } + + function tearDown() { + parent::tearDown(); + context_delete($this->context); + taxonomy_term_delete($this->terms['apples']->tid); + taxonomy_term_delete($this->terms['oranges']->tid); + } + + function test() { + // Apples does trigger the context. + $edit = array( + 'title' => 'Apples', + 'field_tags[und]' => $this->terms['apples']->name + ); + $this->drupalPost('node/add/article', $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($edit['title']); + $this->drupalGet('node/' . $node->nid); + $this->assertText('Active context: testcontext'); + + // Oranges does not trigger the context. + $edit = array( + 'title' => 'Oranges', + 'field_tags[und]' => $this->terms['oranges']->name + ); + $this->drupalPost('node/add/article', $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($edit['title']); + $this->drupalGet('node/' . $node->nid); + $this->assertNoText('Active context: testcontext'); + } +} + +class ContextConditionLanguageTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: language', + 'description' => 'Test language condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'locale'); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages')); + $this->drupalLogin($admin_user); + $this->drupalPost('admin/config/development/performance', array(), t('Clear all caches')); + + // Set up Spanish as second language. + $this->drupalPost('admin/config/regional/language/add', array('langcode' => 'es'), t('Add language')); + $this->drupalPost('admin/config/regional/language/configure', array('language[enabled][locale-url]' => 1), t('Save settings')); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('language' => array('values' => array('es'))); + $context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet('node'); + $this->assertNoText('Active context: testcontext'); + + $this->drupalGet('es/node'); + $this->assertText('Active context: testcontext'); + + // Cleanup + context_delete($context); + } +} + +class ContextConditionSitewideTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: sitewide', + 'description' => 'Test sitewide condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools'); + $admin_user = $this->drupalCreateUser(array('administer site configuration')); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('sitewide' => array('values' => array(1))); + $context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet('node'); + $this->assertText('Active context: testcontext'); + + // Cleanup + context_delete($context); + } +} + +class ContextConditionPathTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: path', + 'description' => 'Test path condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'path'); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer nodes')); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('path' => array('values' => array('admin', 'node/*'))); + $context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet('admin'); + $this->assertText('Active context: testcontext'); + + $node = $this->drupalCreateNode(); + $this->drupalGet("node/{$node->nid}"); + $this->assertText('Active context: testcontext'); + + $this->drupalGet('node'); + $this->assertNoText('Active context: testcontext'); + + // Cleanup + context_delete($context); + + // @TODO: Test with path alias + // @TODO: Test with language prefixes + } +} + +class ContextConditionContextTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: context', + 'description' => 'Test context condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools'); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer nodes')); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('path' => array('values' => array('admin'))); + $context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $subcontext = ctools_export_new_object('context'); + $subcontext->name = 'subcontext'; + $subcontext->conditions = array('context' => array('values' => array('testcontext'))); + $subcontext->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($subcontext); + $this->assertTrue($saved, "Context 'subcontext' saved."); + + $this->drupalGet('admin'); + $this->assertText('Active context: testcontext'); + $this->assertText('Active context: subcontext'); + + // Cleanup + context_delete($context); + + // @TODO: Test exclusion + } +} + +class ContextConditionNodeTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: node', + 'description' => 'Test node condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'blog', 'book'); + $admin_user = $this->drupalCreateUser(array( + 'administer site configuration', + 'administer nodes', + 'create blog content', + 'create book content' + )); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('node' => array('values' => array('blog'))); + $context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet("node/add/blog"); + $this->assertNoText('Active context: testcontext'); + + $this->drupalGet("node/add/book"); + $this->assertNoText('Active context: testcontext'); + + $node = $this->drupalCreateNode(array('type' => 'blog')); + $this->drupalGet("node/{$node->nid}"); + $this->assertText('Active context: testcontext'); + + $node = $this->drupalCreateNode(array('type' => 'book')); + $this->drupalGet("node/{$node->nid}"); + $this->assertNoText('Active context: testcontext'); + + $context->conditions['node']['options']['node_form'] = 1; + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet("node/add/blog"); + $this->assertText('Active context: testcontext'); + + $this->drupalGet("node/add/book"); + $this->assertNoText('Active context: testcontext'); + + // Cleanup + context_delete($context); + } +} + +class ContextConditionMenuTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: menu', + 'description' => 'Test menu condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'blog', 'menu'); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer nodes', 'create blog content')); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('menu' => array('values' => array('node/add'))); + $context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet("node/add/blog"); + $this->assertText('Active context: testcontext'); + + $this->drupalGet("node/add"); + $this->assertText('Active context: testcontext'); + + $this->drupalGet("node"); + $this->assertNoText('Active context: testcontext'); + + // Cleanup + context_delete($context); + } +} + +class ContextConditionBookTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: book', + 'description' => 'Test book condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'book', 'menu'); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer nodes')); + $this->drupalLogin($admin_user); + } + + function test() { + $book = $this->drupalCreateNode(array('type' => 'book', 'book' => array('bid' => 'new'))); + $child = $this->drupalCreateNode(array('type' => 'book', 'book' => array('bid' => $book->nid))); + $dummy = $this->drupalCreateNode(array('type' => 'book')); + + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('book' => array('values' => array(book_menu_name($book->book['bid'])))); + $context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet("node/{$book->nid}"); + $this->assertText('Active context: testcontext'); + + $this->drupalGet("node/{$child->nid}"); + $this->assertText('Active context: testcontext'); + + $this->drupalGet("node/{$dummy->nid}"); + $this->assertNoText('Active context: testcontext'); + + // Cleanup + context_delete($context); + } +} + +class ContextConditionBookroot extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Condition: bookroot', + 'description' => 'Test bookroot condition.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'book', 'menu'); + $admin_user = $this->drupalCreateUser(array( + 'administer site configuration', + 'administer nodes', + 'create book content', + 'edit any book content', + )); + $this->drupalLogin($admin_user); + variable_set('book_allowed_types', array('book', 'page')); + } + + function test() { + $book = $this->drupalCreateNode(array('type' => 'book', 'book' => array('bid' => 'new'))); + $child = $this->drupalCreateNode(array('type' => 'book', 'book' => array('bid' => $book->nid))); + + $dummy = $this->drupalCreateNode(array('type' => 'page', 'book' => array('bid' => 'new'))); + $dummy_child = $this->drupalCreateNode(array('type' => 'page', 'book' => array('bid' => $dummy->nid))); + + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('bookroot' => array('values' => array('book'))); + $context->reactions = array('debug' => array('debug' => TRUE)); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet("node/{$book->nid}"); + $this->assertText('Active context: testcontext'); + + $this->drupalGet("node/{$child->nid}"); + $this->assertText('Active context: testcontext'); + + $this->drupalGet("node/{$dummy->nid}"); + $this->assertNoText('Active context: testcontext'); + + $this->drupalGet("node/{$dummy_child->nid}"); + $this->assertNoText('Active context: testcontext'); + + $this->drupalGet("node/{$book->nid}/edit"); + $this->assertNoText('Active context: testcontext'); + + $context->conditions['bookroot']['options']['node_form'] = 1; + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet("node/{$book->nid}/edit"); + $this->assertText('Active context: testcontext'); + + // Cleanup + context_delete($context); + } +} diff --git a/tests/context.reactions.test b/tests/context.reactions.test new file mode 100644 index 0000000..1ac215f --- /dev/null +++ b/tests/context.reactions.test @@ -0,0 +1,311 @@ + 'Reaction: block', + 'description' => 'Test block reaction.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'block'); + $admin_user = $this->drupalCreateUser(array( + 'administer site configuration', + 'administer blocks' + )); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('sitewide' => array('values' => array(1))); + $context->reactions = array('block' => array('blocks' => array( + 'user-online' => array( + 'module' => 'user', + 'delta' => 'online', + 'region' => 'sidebar_first', + 'weight' => 0, + ), + ))); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + theme_enable(array('bartik')); + variable_set('theme_default', 'bartik'); + $this->refreshVariables(); + + $this->drupalGet('node'); + $this->assertText('Who\'s online'); + + // Test title override of code provided block + $edit = array('title' => 'Context Online Block'); + $this->drupalPost('admin/structure/block/manage/user/online/configure', $edit, t('Save block')); + $this->drupalPost('admin/config/development/performance', array(), t('Clear all caches')); + $this->drupalGet('node'); + $this->assertText('Context Online Block'); + + // Test title of custom block + $edit = array( + 'info' => 'Context Custom Block Info', + 'title' => 'Context Custom Block Title', + 'body[value]' => $this->randomName(32), + ); + $this->drupalPost('admin/structure/block/add', $edit, t('Save block')); + $bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $edit['info']))->fetchField(); + + $context->reactions['block']['blocks']["block-{$bid}"] = array( + 'module' => 'block', + 'delta' => $bid, + 'region' => 'sidebar_first', + 'weight' => 2, + ); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalGet('node'); + $this->assertText('Context Custom Block Title'); + + // Cleanup + context_delete($context); + } +} + +class ContextReactionBlockAjaxTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Reaction: block ajax', + 'description' => 'Test block reaction ajax behavior.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'context_ui', 'ctools'); + $admin_user = $this->drupalCreateUser(array('context ajax block access')); + $this->drupalLogin($admin_user); + } + + function test() { + $this->drupalGet('node', array( + 'query' => array('context_block' => 'user-online,testcontext') + )); + + $this->assertText('"status":1', 'Successful return status.'); + $this->assertText('Who\\u0027s online', 'Expected text in block data.'); + } +} + +class ContextReactionBlockAjaxAccessTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Reaction: block ajax access', + 'description' => 'Test block reaction ajax access behavior.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools'); + $admin_user = $this->drupalCreateUser(); + $this->drupalLogin($admin_user); + } + + function test() { + $this->drupalGet('node', array( + 'query' => array('context_block' => 'user-online,testcontext') + )); + + $this->assertText('"status":0', 'Failed return status.'); + } +} + +class ContextReactionMenuTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Reaction: menu', + 'description' => 'Test menu reaction.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools', 'menu', 'blog'); + $admin_user = $this->drupalCreateUser(array( + 'administer menu', + 'administer nodes', + 'administer site configuration', + 'create blog content', + )); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('sitewide' => array('values' => array(1))); + $context->reactions = array('menu' => 'node/add'); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $this->drupalPost('admin/structure/menu/settings', array('menu_main_links_source' => 'navigation'), 'Save configuration'); + theme_enable(array('bartik')); + variable_set('theme_default', 'bartik'); + $this->refreshVariables(); + + $output = $this->drupalGet('user'); + $url = url('node/add'); + $active = $this->xpath('//li[contains(@class, "active")]/a[@href="' . $url . '"]'); + $this->assertTrue(!empty($active), t('Active menu item found.')); + + // Cleanup + context_delete($context); + } +} + +class ContextReactionBreadcrumbTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Reaction: breadcrumb', + 'description' => 'Test breadcrumb reaction.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools'); + $admin_user = $this->drupalCreateUser(array( + 'access administration pages', + 'administer nodes', + 'administer site configuration' + )); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('path' => array('values' => array('node'))); + $context->reactions = array('breadcrumb' => 'admin/structure'); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + theme_enable(array('bartik')); + variable_set('theme_default', 'bartik'); + $this->refreshVariables(); + + $output = $this->drupalGet('node'); + $this->assertText('Home » Administration » Structure'); + $output = $this->drupalGet('user'); + $this->assertNoText('Home » Administration » Structure'); + + // Cleanup + context_delete($context); + } +} + +class ContextReactionThemeHtmlTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Reaction: theme html', + 'description' => 'Test theme html reaction.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools'); + $admin_user = $this->drupalCreateUser(array( + 'access administration pages', + 'administer nodes', + 'administer site configuration' + )); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('sitewide' => array('values' => array(1))); + $context->reactions = array('theme_html' => array('class' => 'context-test-class')); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $output = $this->drupalGet('node'); + $this->assertRaw('context-test-class'); + + // Cleanup + context_delete($context); + } +} + +class ContextReactionRegionTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Reaction: Region', + 'description' => 'Test Region disable reaction.', + 'group' => 'Context', + ); + } + + function setUp() { + parent::setUp('context', 'ctools'); + $admin_user = $this->drupalCreateUser(array( + 'access administration pages', + 'administer nodes', + 'administer site configuration' + )); + $this->drupalLogin($admin_user); + } + + function test() { + ctools_include('export'); + theme_enable(array('bartik')); + variable_set('theme_default', 'bartik'); + global $theme; + $context = ctools_export_new_object('context'); + $context->name = 'testcontext'; + $context->conditions = array('sitewide' => array('values' => array(1))); + $context->reactions = array( + 'block' => array( + 'blocks' => array( + 'user-online' => array( + 'module' => 'user', + 'delta' => 'online', + 'region' => 'sidebar_first', + 'weight' => '-10', + ), + ), + ), + 'region' => array('bartik' => array('disable' => array('sidebar_first' => 'sidebar_first'))) + ); + $saved = context_save($context); + $this->assertTrue($saved, "Context 'testcontext' saved."); + + $output = $this->drupalGet('node'); + $this->assertNoText("Who's online"); + + // Cleanup + context_delete($context); + } +} diff --git a/tests/context.test b/tests/context.test new file mode 100644 index 0000000..5061d65 --- /dev/null +++ b/tests/context.test @@ -0,0 +1,89 @@ + 'API unit tests', + 'description' => 'Sets all possible context types and checks for integrity.', + 'group' => 'Context', + ); + } + + public function setUp() { + parent::setUp('context'); + } + + public function test() { + // define possible data types + $set_types = array( + 'bool' => TRUE, + 'int' => 1, + 'string' => 'lorem', + 'array' => array('lorem'), + 'object' => new stdClass(), + ); + $id_types = array('int', 'string'); + + // NAMESPACE + foreach ($set_types as $type => $val) { + $set = context_set($val); + // Test return value of context_set() + if (in_array($type, $id_types)) { + // test set integrity + $this->assertIdentical(true, $set, 'Space set successful.'); + // test get integrity + $this->assertIdentical(array(), context_get($val), 'Namespace get successful.'); + $this->assertIdentical(true, context_exists($val), 'Namespace exists successful.'); + } + else { + $this->assertIdentical(false, $set, 'Prohibited namespace not established.'); + } + context_clear(); + } + + // NAMESPACE+ATTRIBUTE + foreach ($set_types as $type => $val) { + foreach ($set_types as $type2 => $val2) { + // test set integrity + $set = context_set($val, $val2); + if (in_array($type, $id_types)) { + // test set integrity + if ($type2 != 'bool') { + $this->assertIdentical(true, $set, 'Namespace and attribute set successful.'); + } + else { + $this->assertIdentical(false, $set); + } + // test get + exists integrity + if (in_array($type2, $id_types)) { + $this->assertIdentical(true, (context_get($val, $val2) == $val2), 'Namespace and attribute get successful.'); + $this->assertIdentical(true, context_exists($val, $val2), 'Namespace and attribute exists.'); + } + elseif (in_array($type2, array('array', 'object'))) { + $this->assertIdentical(true, (context_get($val) == $val2), 'Namespace and attribute get successful.'); + $this->assertIdentical(true, context_exists($val), 'Namespace and attribute exists.'); + } + } + } + context_clear(); + } + + // NAMESPACE+ATTRIBUTE+VALUE, o lord + foreach ($set_types as $type => $val) { + foreach ($set_types as $type2 => $val2) { + foreach ($set_types as $type3 => $val3) { + $set = context_set($val, $val2, $val3); + if (in_array($type, $id_types)) { + if (in_array($type2, $id_types)) { + $this->assertIdentical(true, (context_get($val, $val2, $val3) == $val3), 'Namespace, attribute and value get successful.'); + $this->assertIdentical(true, context_exists($val, $val2, $val3), 'Namespace, attribute and value exists.'); + } + } + context_clear(); + } + } + } + } +} diff --git a/theme/context-block-browser-item.tpl.php b/theme/context-block-browser-item.tpl.php new file mode 100644 index 0000000..788f84c --- /dev/null +++ b/theme/context-block-browser-item.tpl.php @@ -0,0 +1,4 @@ +
+ + +
diff --git a/theme/context-block-browser.tpl.php b/theme/context-block-browser.tpl.php new file mode 100644 index 0000000..69e8a15 --- /dev/null +++ b/theme/context-block-browser.tpl.php @@ -0,0 +1,21 @@ +
+
+ $module_blocks): ?> + + +
+
+ + $block)); ?> + +
+ + + +
+
+
+
+ +
+
diff --git a/theme/context_reaction_block.theme.inc b/theme/context_reaction_block.theme.inc new file mode 100644 index 0000000..4f538bc --- /dev/null +++ b/theme/context_reaction_block.theme.inc @@ -0,0 +1,136 @@ + drupal_render($vars['form']['blocks']), 'class' => array('blocks')), + array('data' => drupal_render($vars['form']['selector']) . drupal_render($vars['form']['block']['help']), 'class' => array('selector')), + ); + + $output = drupal_render_children($vars['form']); + + $table = array( + 'rows' => array($row), + 'attributes' => array('id' => 'context-blockform'), + ); + $output .= theme('table', $table); + return $output; +} + +/** + * Generates the AJAX enabled block administration portion of the context_ui admin form. + */ +function theme_context_block_regions_form($vars) { + $form = $vars['form']; + + // Add draggable weights + drupal_add_js('misc/tableheader.js'); + drupal_add_js(drupal_get_path('module', 'context') . '/plugins/context_reaction_block.js'); + drupal_add_css(drupal_get_path('module', 'context') . '/plugins/context_reaction_block.css'); + + $output = ''; + + foreach (element_children($form) as $region) { + $attr = array( + 'id' => "context-blockform-region-{$region}", + 'class' => array("context-blockform-region"), + ); + drupal_add_tabledrag($attr['id'], 'order', 'sibling', 'tabledrag-hide', NULL, NULL, FALSE); + $rows = array(); + foreach (element_children($form[$region]) as $id) { + $form[$region][$id]['weight']['#attributes'] = array('class' => array('tabledrag-hide')); + $label = $form[$region][$id]['#value']; + $remove = l(t('X'), $_GET['q'], array('fragment' => 'remove', 'attributes' => array('class' => array('remove')))); + $rows[] = array( + 'data' => array($label, drupal_render($form[$region][$id]['weight']), $remove), + 'class' => array('draggable'), + 'id' => $id, + ); + } + $output .= "
"; + $output .= l(t('+') . ' ' . t('Add'), $_GET['q'], array('fragment' => $region, 'attributes' => array('class' => array('add-block')))); + $output .= $form[$region]['#title']; + $output .= "
"; + $output .= theme('table', array('rows' => $rows, 'attributes' => $attr)); + } + return $output; +} + +/** + * Use placeholder content for script tags that need to be replaced. + */ +function theme_context_block_script_placeholder($vars) { + $text = $vars['text']; + $message = t('Please reload the page to view this block.'); + return "
{$text}
{$message}
"; +} + +/** + * Preprocessor for theme('context_block_browser'). + */ +function template_preprocess_context_block_browser(&$vars) { + $categories = array( + '#type' => 'select', + '#options' => array(0 => '<' . t('All Categories') . '>'), + '#attributes' => array('class' => array('context-block-browser-categories')), + '#value' => 0, + '#size' => 1, + '#id' => '', + '#name' => '', + '#parents' => array(''), + '#multiple' => FALSE, + '#required' => FALSE, + ); + $blocks = array(); + // Group blocks by module. + foreach ($vars['blocks'] as $block) { + $group = isset($block->context_group) ? $block->context_group : $block->module; + // Normalize the $group, borrowed from drupal_html_id + $group = strtr(drupal_strtolower($group), array(' ' => '-', '_' => '-', '[' => '-', ']' => '')); + if (!isset($categories[$group])) { + $info = system_get_info('module', $block->module); + $title = isset($block->context_group) ? $block->context_group : (!empty($info['name']) ? $info['name'] : $block->module); + $categories['#options'][$group] = $title; + } + $blocks[$group][$block->bid] = $block; // Don't call theme('context_block_browser_item') to allow others to alter. + } + + //add help text to tell people how to use the block browser + $help_text = array( + '#prefix' => '
', + '#markup' => t('To add a block to the current region, simply click on the block. You may use the category filter to filter by + block type or the search filter to find the block that you wish to add.'), + '#suffix' => '
', + ); + + $filter_label = array( + '#prefix' => '
', + '#markup' => t('Search Filter'), + '#suffix' => '
', + ); + + $vars['categories'] = $categories; // Don't call theme('select') here to allow further preprocesses to alter the element. + $vars['blocks'] = $blocks; + $vars['help_text'] = $help_text; + $vars['filter_label'] = $filter_label; +} + +/** + * Preprocessor for theme('context_block_browser_item'). + */ +function template_preprocess_context_block_browser_item(&$vars) { + $vars['bid'] = $vars['block']->bid; + $vars['info'] = check_plain($vars['block']->info); +} + +/** + * Theme wrapper for editable blocks. + * + * @ingroup themeable + */ +function theme_context_block_edit_wrap($vars) { + $block = $vars['element']['#block']; + return $vars['element']['#children'] . ""; +} -- GitLab