Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1023 diff -u -p -r1.1023 common.inc --- includes/common.inc 16 Oct 2009 19:20:33 -0000 1.1023 +++ includes/common.inc 16 Oct 2009 21:45:27 -0000 @@ -3292,7 +3292,7 @@ function drupal_clear_css_cache() { * @return * The cleaned identifier. */ -function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_' => '-', '[' => '-', ']' => '')) { +function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '')) { // By default, we filter using Drupal's coding standards. $identifier = strtr($identifier, $filter); Index: includes/menu.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/menu.inc,v retrieving revision 1.354 diff -u -p -r1.354 menu.inc --- includes/menu.inc 16 Oct 2009 19:06:21 -0000 1.354 +++ includes/menu.inc 17 Oct 2009 02:14:31 -0000 @@ -663,14 +663,14 @@ function _menu_item_localize(&$item, $ma * a non existing node) then this function return FALSE. */ function _menu_translate(&$router_item, $map, $to_arg = FALSE) { - if ($to_arg) { + if ($to_arg && !empty($router_item['to_arg_functions'])) { // Fill in missing path elements, such as the current uid. _menu_link_map_translate($map, $router_item['to_arg_functions']); } // The $path_map saves the pieces of the path as strings, while elements in // $map may be replaced with loaded objects. $path_map = $map; - if (!_menu_load_objects($router_item, $map)) { + if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) { // An error occurred loading an object. $router_item['access'] = FALSE; return FALSE; @@ -706,17 +706,15 @@ function _menu_translate(&$router_item, * An array of helper function (ex: array(2 => 'menu_tail_to_arg')) */ function _menu_link_map_translate(&$map, $to_arg_functions) { - if ($to_arg_functions) { - $to_arg_functions = unserialize($to_arg_functions); - foreach ($to_arg_functions as $index => $function) { - // Translate place-holders into real values. - $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index); - if (!empty($map[$index]) || isset($arg)) { - $map[$index] = $arg; - } - else { - unset($map[$index]); - } + $to_arg_functions = unserialize($to_arg_functions); + foreach ($to_arg_functions as $index => $function) { + // Translate place-holders into real values. + $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index); + if (!empty($map[$index]) || isset($arg)) { + $map[$index] = $arg; + } + else { + unset($map[$index]); } } } @@ -751,7 +749,9 @@ function _menu_link_translate(&$item) { } else { $map = explode('/', $item['link_path']); - _menu_link_map_translate($map, $item['to_arg_functions']); + if (!empty($item['to_arg_functions'])) { + _menu_link_map_translate($map, $item['to_arg_functions']); + } $item['href'] = implode('/', $map); // Note - skip callbacks without real values for their arguments. @@ -761,7 +761,7 @@ function _menu_link_translate(&$item) { } // menu_tree_check_access() may set this ahead of time for links to nodes. if (!isset($item['access'])) { - if (!_menu_load_objects($item, $map)) { + if (!empty($item['load_functions']) && !_menu_load_objects($item, $map)) { // An error occurred loading an object. $item['access'] = FALSE; return FALSE; @@ -1614,10 +1614,11 @@ function menu_local_tasks($level = 0) { $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC)) ->fields('menu_router') ->condition('tab_root', $router_item['tab_root']) + ->condition('tab_type', 'context', '<>') ->orderBy('weight') ->orderBy('title') ->execute(); - $map = arg(); + $map = $router_item['original_map']; $children = array(); $tasks = array(); $root_path = $router_item['path']; @@ -1758,6 +1759,108 @@ function menu_local_tasks($level = 0) { } /** + * Retrieve contextual links for a system object based on registered local tasks. + * + * This leverages the menu system to retrieve the first layer of registered + * local tasks for a given system path. All local tasks of the tab type 'task' + * or 'context' are taken into account. + * + * @see hook_menu() + * + * For example, when considering the following registered local tasks: + * - node/%node/view (default local task) with no tab_type + * - node/%node/edit with tab_type "task" + * - node/%node/revisions with tab_type "view" + * - node/%node/report-as-spam with tab_type "context" + * + * If the path "node/123" is passed to this function, then it will return the + * links for 'edit' and 'report-as-spam'. + * + * @param $path + * The menu router path of the object to retrieve local tasks for, for example + * "node/123" or "admin/structure/menu/manage/[menu_name]". + * + * @return + * A list of menu router items that are local tasks for the passed in path. + * + * @see system_preprocess() + */ +function menu_contextual_links($parent_path, $args) { + static $path_empty = array(); + + // @todo This access check really doesn't belong into an API function. But as + // of now, there is no better location to entirely prevent the loading and + // building of contextual links in case they should not be rendered at all. + if (!user_access('access contextual links')) { + return array(); + } + + $links = array(); + // Performance: In case a previous invocation for the same parent path did not + // return any links, we immediately return here. + if (isset($path_empty[$parent_path])) { + return $links; + } + // Construct the item-specific parent path. + $path = $parent_path . '/' . implode('/', $args); + + // Get the router item for the given parent link path. + $router_item = menu_get_item($path); +// krumo($router_item); + if (!$router_item || !$router_item['access']) { + $path_empty[$parent_path] = TRUE; + return $links; + } + $data = &drupal_static(__FUNCTION__, array()); + $root_path = $router_item['path']; + + // Performance: For a single, normalized path (such as 'node/%') we only query + // available tasks once per request. + if (!isset($data[$root_path])) { + // Get all contextual links that are direct children of the router item and + // not of the tab type 'view'. + $data[$root_path] = db_select('menu_router', 'm') + ->fields('m') + ->condition('tab_parent', $router_item['tab_root']) + ->condition('tab_type', 'view', '<>') + ->orderBy('weight') + ->orderBy('title') + ->execute() + ->fetchAllAssoc('path', PDO::FETCH_ASSOC); + } + $parent_length = drupal_strlen($root_path) + 1; + $map = $router_item['original_map']; + foreach ($data[$root_path] as $item) { + // Extract the actual "task" string from the path argument. + $key = drupal_substr($item['path'], $parent_length); + + // Denormalize and translate the contextual link. + _menu_translate($item, $map, TRUE); + if (!$item['access']) { + continue; + } + // All contextual links are keyed by the actual "task" path argument. The + // menu system does not allow for two local tasks with the same name, and + // since the key is also used as CSS class for the link item, which may be + // styled as icon, it wouldn't make sense to display the same icon for + // different tasks. + $links[$key] = $item; + } + + // Allow modules to alter contextual links. + drupal_alter('menu_contextual_links', $links, $router_item, $root_path); + + // Performance: If the current user does not have access to any links for this + // router path and no other module added further links, we assign FALSE here + // to skip the entire process the next time the same router path is requested. + if (empty($links)) { + $path_empty[$parent_path] = TRUE; + } + + return $links; +} + +/** * Returns the rendered local tasks at the top level. */ function menu_primary_local_tasks() { @@ -2829,6 +2932,10 @@ function _menu_router_build($callbacks) $item['tab_parent'] = ''; $item['tab_root'] = $path; } + // If not specified, assign the default tab type for local tasks. + elseif (!isset($item['tab_type'])) { + $item['tab_type'] = 'view'; + } for ($i = $item['_number_parts'] - 1; $i; $i--) { $parent_path = implode('/', array_slice($item['_parts'], 0, $i)); if (isset($menu[$parent_path])) { @@ -2851,6 +2958,10 @@ function _menu_router_build($callbacks) $item['access arguments'] = $parent['access arguments']; } } + // Same for load arguments. + if (!isset($item['load arguments']) && isset($parent['load arguments'])) { + $item['load arguments'] = $parent['load arguments']; + } // Same for page callbacks. if (!isset($item['page callback']) && isset($parent['page callback'])) { $item['page callback'] = $parent['page callback']; @@ -2901,6 +3012,7 @@ function _menu_router_build($callbacks) 'theme callback' => '', 'description' => '', 'position' => '', + 'tab_type' => '', 'tab_parent' => '', 'tab_root' => $path, 'path' => $path, @@ -2944,6 +3056,7 @@ function _menu_router_save($menu, $masks 'delivery_callback', 'fit', 'number_parts', + 'tab_type', 'tab_parent', 'tab_root', 'title', @@ -2972,6 +3085,7 @@ function _menu_router_save($menu, $masks 'delivery_callback' => $item['delivery callback'], 'fit' => $item['_fit'], 'number_parts' => $item['_number_parts'], + 'tab_type' => $item['tab_type'], 'tab_parent' => $item['tab_parent'], 'tab_root' => $item['tab_root'], 'title' => $item['title'], Index: misc/contextual_links.css =================================================================== RCS file: misc/contextual_links.css diff -N misc/contextual_links.css --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ misc/contextual_links.css 16 Oct 2009 17:59:53 -0000 @@ -0,0 +1,38 @@ +/* $Id$ */ + +/** + * Contextual links regions. + */ +.contextual-links-region { + outline: none; + position: relative; +} +.contextual-links-region-active { + outline: #000 dashed 1px; +} + +/** + * Contextual links. + */ +ul.contextual-links { + float: right; + font-size: 90%; + margin: 0; + padding: 0; +} +ul.contextual-links li { + border-left: 1px solid #ccc; + display: inline; + line-height: 100%; + list-style: none; + margin: 0 0 0 0.3em; + padding: 0 0 0 0.6em; +} +ul.contextual-links li.first { + border-left: 0; + margin: 0; + padding: 0; +} +ul.contextual-links li a { + text-decoration: none; +} Index: misc/contextual_links.js =================================================================== RCS file: misc/contextual_links.js diff -N misc/contextual_links.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ misc/contextual_links.js 16 Oct 2009 18:01:08 -0000 @@ -0,0 +1,33 @@ +// $Id$ +(function ($) { + +Drupal.contextualLinks = Drupal.contextualLinks || {}; + +/** + * Attach outline behavior for regions associated with contextual links. + */ +Drupal.behaviors.contextualLinks = { + attach: function (context) { + $('ul.contextual-links', context).once('contextual-links', function () { + $(this).hover(Drupal.contextualLinks.hover, Drupal.contextualLinks.hoverOut); + }); + } +}; + +/** + * Enables outline for the region contextual links are associated with. + */ +Drupal.contextualLinks.hover = function () { + $(this).addClass('contextual-links-link-active') + .closest('.contextual-links-region').addClass('contextual-links-region-active'); +}; + +/** + * Disables outline for the region contextual links are associated with. + */ +Drupal.contextualLinks.hoverOut = function () { + $(this).removeClass('contextual-links-link-active') + .closest('.contextual-links-region').removeClass('contextual-links-region-active'); +}; + +})(jQuery); Index: modules/block/block.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.api.php,v retrieving revision 1.8 diff -u -p -r1.8 block.api.php --- modules/block/block.api.php 31 Aug 2009 17:06:08 -0000 1.8 +++ modules/block/block.api.php 16 Oct 2009 17:50:23 -0000 @@ -140,6 +140,7 @@ function hook_block_view($delta = '') { 'content' => mymodule_display_block_exciting(), ); break; + case 'amazing': $block = array( 'subject' => t('Default title of the amazing block'), @@ -151,6 +152,79 @@ function hook_block_view($delta = '') { } /** + * Perform alterations to the content of a block. + * + * This hook allows you to modify any data returned by hook_block_view(). + * + * Note that instead of hook_block_view_alter(), which is called for all + * blocks, you can also use hook_block_view_MODULE_DELTA_alter() to alter a + * specific block. + * + * @param $data + * An array of data, as returned from the hook_block_view() implementation of + * the module that defined the block: + * - subject: The localized title of the block. + * - content: Either a string or a renderable array representing the content + * of the block. You should check that the content is an array before trying + * to modify parts of the renderable structure. + * @param $block + * The block object, as loaded from the database, having the main properties: + * - module: The name of the module that defined the block. + * - delta: The identifier for the block within that module, as defined within + * hook_block_info(). + * + * @see hook_block_view_alter() + * @see hook_block_view() + */ +function hook_block_view_alter(&$data, $block) { + // Remove the contextual links on all blocks that provide them. + if (is_array($data['content']) && isset($data['content']['#contextual_links'])) { + unset($data['content']['#contextual_links']); + } + // Add a theme wrapper function defined by the current module to all blocks + // provided by the "somemodule" module. + if (is_array($data['content']) && $block->module == 'somemodule') { + $data['content']['#theme_wrappers'][] = 'mymodule_special_block'; + } +} + +/** + * Perform alterations to a specific block. + * + * Modules can implement hook_block_view_MODULE_DELTA_alter() to modify a + * specific block, rather than implementing hook_block_view_alter(). + * + * Note that this hook fires before hook_block_view_alter(). Therefore, all + * implementations of hook_block_view_MODULE_DELTA_alter() will run before all + * implementations of hook_block_view_alter(), regardless of the module order. + * + * @param $data + * An array of data, as returned from the hook_block_view() implementation of + * the module that defined the block: + * - subject: The localized title of the block. + * - content: Either a string or a renderable array representing the content + * of the block. You should check that the content is an array before trying + * to modify parts of the renderable structure. + * @param $block + * The block object, as loaded from the database, having the main properties: + * - module: The name of the module that defined the block. + * - delta: The identifier for the block within that module, as defined within + * hook_block_info(). + * + * @see hook_block_view_alter() + * @see hook_block_view() + */ +function hook_block_view_MODULE_DELTA_alter(&$data, $block) { + // This code will only run for a specific block. For example, if MODULE_DELTA + // in the function definition above is set to "mymodule_somedelta", the code + // will only run on the "somedelta" block provided by the "mymodule" module. + + // Change the title of the "somedelta" block provided by the "mymodule" + // module. + $data['subject'] = t('New title of the block'); +} + +/** * Act on blocks prior to rendering. * * This hook allows you to add, remove or modify blocks in the block list. The Index: modules/block/block.module =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.module,v retrieving revision 1.391 diff -u -p -r1.391 block.module --- modules/block/block.module 16 Oct 2009 23:48:37 -0000 1.391 +++ modules/block/block.module 17 Oct 2009 02:43:07 -0000 @@ -95,15 +95,20 @@ function block_menu() { 'type' => MENU_CALLBACK, 'file' => 'block.admin.inc', ); - $items['admin/structure/block/manage/%block/%/configure'] = array( + $items['admin/structure/block/manage/%block/%'] = array( 'title' => 'Configure block', 'page callback' => 'drupal_get_form', 'page arguments' => array('block_admin_configure', 4), 'load arguments' => array(5), 'access arguments' => array('administer blocks'), - 'type' => MENU_CALLBACK, 'file' => 'block.admin.inc', ); + $items['admin/structure/block/manage/%block/%/configure'] = array( + 'title' => 'Configure block', + 'load arguments' => array(5), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'tab_type' => 'context', + ); $items['admin/structure/block/manage/%block/%/delete'] = array( 'title' => 'Delete block', 'page callback' => 'drupal_get_form', @@ -282,6 +287,12 @@ function _block_get_renderable_array($li foreach ($list as $key => $block) { $build[$key] = $block->content; unset($block->content); + + // Add contextual links for this block; skipping the system main block. + if ($key != 'system_main') { + $build[$key]['#contextual_links']['block'] = menu_contextual_links('admin/structure/block/manage', array($block->module, $block->delta)); + } + $build[$key] += array( '#block' => $block, '#weight' => ++$weight, @@ -785,6 +796,12 @@ function _block_render_blocks($region_bl } else { $array = module_invoke($block->module, 'block_view', $block->delta); + + // Allow modules to modify the block before it is viewed, via either + // hook_block_view_MODULE_DELTA_alter() or hook_block_view_alter(). + drupal_alter("block_view_{$block->module}_{$block->delta}", $array, $block); + drupal_alter('block_view', $array, $block); + if (isset($cid)) { cache_set($cid, $array, 'cache_block', CACHE_TEMPORARY); } Index: modules/block/block.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.tpl.php,v retrieving revision 1.4 diff -u -p -r1.4 block.tpl.php --- modules/block/block.tpl.php 11 Sep 2009 06:48:02 -0000 1.4 +++ modules/block/block.tpl.php 16 Oct 2009 17:52:58 -0000 @@ -11,6 +11,7 @@ * - $block->module: Module that generated the block. * - $block->delta: An ID for the block, unique within each module. * - $block->region: The block region embedding the current block. + * - $contextual_links (array): An array of contextual links for the block. * - $classes: String of classes that can be used to style contextually through * CSS. It can be manipulated through the variable $classes_array from * preprocess functions. The default values can be one or more of the following: @@ -36,6 +37,11 @@ */ ?>