>Site building>>Triggers. Pick a tab. Click any link on the * screen and return to the Triggers page. The previously selected tab will * be pre-selected for you. * Double row of tabs: * o First visit Administer>>Site building>>Themes. Click the Configure tab * and pick a theme tab. Click away to, say Blocks, then return to Themes. * The Configure and theme tabs are pre-selected for you. * o First visit My account, click the Edit tab, then Smart tabs. Return to * the main navigation menu. Then revisit 'My account' to find Edit, 'Smart * tabs' still selected. * o Install Module Grants (http://drupal.org/project/module_grants), which * will create an 'Accessible content' menu item. First visit * accessible-content/i-can-edit/published, then accessible-content/i-can-view. * The Published tab selection will carry across to the new tab. * * You may occasionally find that certain rows of tabs don't exhibit the * expected Smart behaviour. This can happen when a programmer has * deviated from the convention. * In some cases a workaround may be possible, see smart_tab_fixes.inc. * If you've come across tabs that don't perform Smartly, let us know on the * issues pages of the Smart menus project. * While waiting for a fix for the particular tabs in question, you can * suppress Smart behaviour for selected pages here: Administer>>Site * configuration>>Smart tabs. */ require_once drupal_get_path('module', 'smart_menus') .'/smart_tabs.install'; require_once drupal_get_path('module', 'smart_menus') .'/smart_tabs_fixes.inc'; define('SMART_TABS_DEFAULT_PAGE_EXCLUSIONS', "admin/content/node-type/*/fields*\nnode/*/edit/components*\nnode/*/webform/components/new\nsearch"); define('SMART_TABS_AUTOSELECT_DEFAULT_TAB_EXCLUSIONS', 'admin/content/node-type/*/fields*'); /** * Implementation of hook_help(). */ function smart_tabs_help($path, $arg) { switch ($path) { case 'admin/help#smart_menus': $s = t('Adds memory to your tabs so that on subsequent visits to the same page the tab last visited is again pre-selected for you.'); break; } return empty($s) ? '' : '

'. $s .'

'; } /** * Implementation of hook_perm(). */ function smart_tabs_perm() { return array('administer Smart tabs personal settings'); } /** * Implementation of hook_menu(). * * Define configuration options for Smart tabs. */ function smart_tabs_menu() { $items = array(); $items['admin/settings/smart_tabs'] = array( 'title' => 'Smart tabs', 'description' => 'Configure Smart tabs behaviour', 'page callback' => 'drupal_get_form', 'page arguments' => array('_smart_tabs_admin_settings'), 'access arguments' => array('administer site configuration'), ); return $items; } /** * Menu callback for admin settings. */ function _smart_tabs_admin_settings() { $form['smart_tabs_theme_override'] = array( '#type' => 'checkbox', '#title' => t('Allow Smart tabs to control tab behaviour'), '#default_value' => variable_get('smart_tabs_theme_override', TRUE), '#description' => t('Only untick this if Smart tabs does not behave as advertised with your theme. If you untick this option, but still want Smart tabs behaviour, you must implement either yourtheme_menu_local_tasks() or phptemplate_menu_local_tasks() yourself.'), ); $form['smart_tabs_pages'] = array( '#type' => 'fieldset', '#title' => t('Specify the pages on which tabs should behave smartly'), '#collapsible' => FALSE, ); $form['smart_tabs_pages']['smart_tabs_include_pages'] = array( '#type' => 'textarea', '#title' => t('Pages to include'), '#default_value' => variable_get('smart_tabs_include_pages', '*'), '#description' => t("Enter Drupal menu paths, one per line. The asterisk '*' is the wildcard character. An example is %admin-wildcard for all administration pages.", array('%admin-wildcard' => 'admin/*')), ); $form['smart_tabs_pages']['smart_tabs_exclude_pages'] = array( '#type' => 'textarea', '#title' => t('Pages to exclude from the collection specified above'), '#default_value' => variable_get('smart_tabs_exclude_pages', SMART_TABS_DEFAULT_PAGE_EXCLUSIONS), '#description' => t("Enter Drupal menu paths, one per line, for instance: %node. The asterisk '*' is the wildcard character allowing you to specify a groups of nodes, e.g. %node-wildcard. Where they exist use the URL aliases rather than the node numbers. %front is the front page.", array('%node' => 'recipes/desserts/chocolate-cake', '%node-wildcard' => 'recipes/desserts/*', '%front' => '')) ); $form['smart_tabs_missing_default'] = array( '#type' => 'fieldset', '#title' => t('Specify the pages on which Smart tabs should not select a default tab when none is specified.'), '#collapsible' => FALSE, ); $form['smart_tabs_missing_default']['smart_tabs_autoselect_default_tab_exclusions'] = array( '#type' => 'textarea', '#title' => t('Pages to exclude'), '#default_value' => variable_get('smart_tabs_autoselect_default_tab_exclusions', SMART_TABS_AUTOSELECT_DEFAULT_TAB_EXCLUSIONS), '#description' => t('Same notation as above applies.') ); $form['smart_tabs_debug'] = array( '#type' => 'checkbox', '#title' => t('Enable debug info'), '#default_value' => variable_get('smart_tabs_debug', FALSE), '#description' => t('Debug info is visible only to a logged-in administrator (uid=1).'), ); return system_settings_form($form); } /** * Implementation of hook_theme(). * * Declare the theme functions used in this module. */ function smart_tabs_theme() { return array('menu_local_tasks_smart_tabs' => array('arguments' => array())); } /** * Theme the menu local tasks in a smart way. * * @return */ function theme_menu_local_tasks_smart_tabs() { $output = ''; if ($primary_tabs = menu_primary_local_tasks_smart_tabs()) { $output .= "\n"; } if ($secondary_tabs = menu_secondary_local_tasks_smart_tabs()) { $output .= "\n"; } return $output; } /* Implementation of ENGINENAME_hook(). * * Returns the rendered local tasks, overriding theme_menu_local_tasks(). * Note: Smart tabs deals with this altering the registry directly, thus * avoiding clashes with themes like Garland, which already implement * ENGINENAME_hook(). See smart_tabs_fixes.inc * * @ingroup themeable * function phptemplate_menu_local_tasks() { return theme(array('menu_local_tasks_smart_tabs', 'menu_local_tasks')); }*/ function menu_primary_local_tasks_smart_tabs() { return _smart_tabs_menu_local_tasks(0); } function menu_secondary_local_tasks_smart_tabs() { return _smart_tabs_menu_local_tasks(1); } /** * Implementation of hook_user(). */ function smart_tabs_user($op, &$edit, &$account, $category = NULL) { switch ($op) { case 'categories': // Get here once when module is first enabled return array(array( 'name' => 'smart tabs', 'title' => t('Smart tabs'), 'access callback' => 'user_access', 'access arguments' => array('administer Smart tabs personal settings'), 'weight' => 10, // same as Smart menus )); case 'form': if ($category == 'smart tabs') { return _smart_tabs_user_profile_form($edit); } } } /** * Helper function to add Smart tabs personal settings form to the user profile, * i.e. 'My account' page (Edit tab). */ function _smart_tabs_user_profile_form($edit) { $form = array(); $form['smart_tabs'] = array( '#type' => 'fieldset', '#title' => t('Smart tabs settings'), '#collapsible' => FALSE, '#collapsed' => FALSE, ); $default = isset($edit['profile_smart_tabs_disable']) ? $edit['profile_smart_tabs_disable'] : FALSE; $form['smart_tabs']['profile_smart_tabs_disable'] = array( '#type' => 'checkbox', '#title' => t('Disable Smart tabs'), '#default_value' => $default, '#description' => t('Unless disabled here, your tabs will have a memory so that upon return to a page the previously selected tabs are again selected for you.'), ); return $form; } /** * Render the local tasks (aka tabs). Similar to menu_local_tasks(), but smarter. * * 1) Smart tabs have a memory, so that when you return to the page with the * tabs, your last last tab selection will again be active. * 2) Where two rows of tabs are used, i.e. primary and secondary, smart * secondary tabs are aware not only of their parents, but also of their cousins. * By this it is meant that if you select a new primary tab and this tab has * a secondary tab by the same leaf name as the previous selected secondary * tab, then this secondary tab will be auto-selected. * This results in a far more natural user experience, as only one tab * changes per mouse click, rather than two. * * @param $level * The level of tasks you ask for. Primary tasks are 0, secondary are 1. * @return * Themed output corresponding to the tabs of the requested level. */ function _smart_tabs_menu_local_tasks($level = 0) { static $tabs; if (!_smart_tabs_user_has_smart_tabs_enabled()) { return menu_local_tasks($level); } if (!isset($tabs)) { $router_item = menu_get_item(); if (!$router_item || !$router_item['access']) { return ''; } // Get all primary and secondary tabs regardless of $level $result = db_query("SELECT * FROM {menu_router} WHERE tab_root = '%s' ORDER BY weight, title", $router_item['tab_root']); $map = arg(); $children = array(); $tasks = array(); while ($item = db_fetch_array($result)) { _menu_translate($item, $map, TRUE); if ($item['tab_parent']) { $children[$item['tab_parent']][$item['path']] = $item; } // Store the translated item for later use. $tasks[$item['path']] = $item; } $path = $router_item['path']; // $path may include % whereas href won't $is_included = _smart_tabs_auto_select_tab($path, $router_item['href'], $tasks); $tabs = array(); // Find all tabs below the current path. // Tab parenting may skip levels, so number of parts in the path may not // equal depth. Thus we use the $depth counter (offset by 1000 for ksort). $depth = 1001; $new_active_tabs = array(); while (isset($children[$path])) { // A quick check to see if a static default tab has been set. // This will almost always be the case except for Module Grants' // accessible-content menu. _smart_tabs_set_default_tab_if_necessary($path, $children[$path]); $tabs_current = ''; $next_path = ''; $count = 0; foreach ($children[$path] as $item) { if ($item['access']) { $count++; $is_active_tab = ($item['type'] == MENU_DEFAULT_LOCAL_TASK); if ($is_active_tab) { $item['localized_options']['attributes']['class'] = 'active'; $new_active_tabs[$item['tab_parent']] = $item['path']; // Set this tab as the one whose children are to be iterated over next $next_path = $item['path']; } // Add the themed tab to the current row $link = _smart_tabs_generate_link($item, $tasks); $tabs_current .= theme('menu_local_task', $link, $is_active_tab); } } $path = $next_path; $tabs[$depth]['count'] = $count; $tabs[$depth]['output'] = $tabs_current; $depth++; } // Find all tabs at the same level or above the current one. $parent = $router_item['tab_parent']; $path = $router_item['path']; $current = $router_item; $depth = 1000; while (isset($children[$parent])) { $tabs_current = ''; $next_path = ''; $next_parent = ''; $count = 0; foreach ($children[$parent] as $item) { if ($item['access']) { $count++; // Check for the clicked (i.e. active) tab. $is_active_tab = ($item['path'] == $path); if ($is_active_tab) { $new_active_tabs[$item['tab_parent']] = $item['path']; $next_path = $item['tab_parent']; if (isset($tasks[$next_path])) { $next_parent = $tasks[$next_path]['tab_parent']; } } $link = _smart_tabs_generate_link($item, $tasks); $tabs_current .= theme('menu_local_task', $link, $is_active_tab); } } $path = $next_path; $parent = $next_parent; $tabs[$depth]['count'] = $count; $tabs[$depth]['output'] = $tabs_current; $depth--; } // Sort by depth. ksort($tabs); // Remove the depth, we are interested only in their relative placement. $tabs = array_values($tabs); if ($is_included) { // Update the session with the currently selected tabs (1 for each row) _smart_tabs_remember_tabs($new_active_tabs); } } // Do not display single tabs. return (isset($tabs[$level]) && $tabs[$level]['count'] > 1) ? $tabs[$level]['output'] : ''; } function _smart_tabs_user_has_smart_tabs_enabled() { global $user; return !($user->profile_smart_tabs_disable); } function _smart_tabs_auto_select_tab($path, $href, $tasks) { // Note that the Edit, Revisions, Track tabs aren't aliased so will // have $href==$href_alias like node/123/edit, node/123/revisions etc. $href_alias = drupal_get_path_alias($href); $include_items = variable_get('smart_tabs_include_pages', '*'); $exclude_items = variable_get('smart_tabs_exclude_pages', SMART_TABS_DEFAULT_PAGE_EXCLUSIONS); $include_item = drupal_match_path($href_alias, $include_items); $exclude_item = drupal_match_path($href_alias, $exclude_items); $is_included = $include_item && !$exclude_item; if (!$is_included) { _st_debug_info(t('%page is excluded from Smart tabs', array('%page' => $href_alias))); } else { $new_active_href = _smart_tabs_derive_new_active_href($path, $tasks); if (_smart_tabs_is_valid($new_active_href)) { if (drupal_match_path($new_active_href, $exclude_items)) { _st_debug_info(t('Not auto-selecting %tab_name tab at %tab_href as it is on the Smart tabs exclusion list.', array( '%tab_name' => drupal_ucfirst(_smart_tabs_leaf($new_active_href)), '%tab_href' => _smart_tabs_parent_path($new_active_href)))); $is_included = FALSE; } else { // @TODO check if user has access to new_active_ref // Eg not all nodes come with a Revisions tab, so 'Access denied' may occur. // Can we do this via $tasks ? // We're redirecting to the previously active child or grandchild, // discarding the page just loaded (but not yet themed, so not visible). // It would be nicer if we could execute this code BEFORE loading the // page, but until this is dealt with properly in the core architecture // we'll have to just work around it. // @TODO change @tab_name to be the localised (translated) tab name. _st_debug_info(t('Auto-selecting previously active %tab_name tab at %tab_href', array( '%tab_name' => drupal_ucfirst(_smart_tabs_leaf($new_active_href)), '%tab_href' => _smart_tabs_parent_path($new_active_href)))); drupal_goto($new_active_href); } } elseif (!empty($new_active_href)) { _st_debug_info(t('The href %href is invalid.', array('%href' => $new_active_href))); } $_SESSION['active-tabs']['last-active-href'] = $href_alias; } return $is_included; } function _smart_tabs_set_default_tab_if_necessary($path, &$children) { foreach (array_reverse($children) as $tab => $item) { if ($item['access']) { if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) { // we're ok, get out return; } $accessible_tab = $tab; } } if (empty($accessible_tab)) { _st_debug_info(t('%path does not have any tabs or tabs accessible to you.', array('%path' => $path))); return; } _st_debug_info(t('No default tab is set for local task %path.', array('%path' => $path))); $exclude_items = variable_get('smart_tabs_autoselect_default_tab_exclusions', SMART_TABS_AUTOSELECT_DEFAULT_TAB_EXCLUSIONS); $exclude_item = drupal_match_path($path, $exclude_items); if ($exclude_item) { _st_debug_info(t('Not pre-selecting tab %accessible_tab, as auto-selection is switched-off for the tabs on page %path.', array('%accessible_tab' => $accessible_tab, '%path' => $path))); } else { _st_debug_info(t('Selecting default tab %accessible_tab for local task %path.', array('%accessible_tab' => $accessible_tab, '%path' => $path))); $children[$accessible_tab]['type'] = MENU_DEFAULT_LOCAL_TASK; } } function _smart_tabs_is_valid($path) { return !empty($path) && (strpos($path, '//') === FALSE); } /** * Find out where to go based on the clicked menu item or tab. * The clicked item could be * 1) a menu item that is the parent to one or more rows of local tasks (aka tabs) * 2) a local task that * a) has zero or more sibling tasks but no child tasks (ie a single row of tabs) * b) has zero or more sibling tasks as well as child tasks (ie a 2nd row of tabs) * * @param $clicked_path * The router path as opposed to the href, e.g. 'user/%' rather than 'user/34' * @param $tasks * The row or two rows of local tasks as loaded from the database * @return * o if the clicked menu item or local task has a recently active grandchild * task, return that grandchild * o else, if the clicked menu item or local task has a recently active child * task, return that child * o else, if the clicked item is a local task, return the child that has the * same leaf path name as the previously selected nephew task (ie. the child * of a sibling of the clicked task), if it exists. * Example: * If accessible-content/i-can-edit/published was selected previously * and the user clicks accessible-content/i-can-view, then return * accessible-content/i-can-view/published * o else return void */ function _smart_tabs_derive_new_active_href($clicked_path, $tasks) { $active_child = $_SESSION['active-tabs'][$clicked_path]; if (empty($active_child)) { // eg accessible-content/i-can-view clicked for first time $parent = $tasks[$clicked_path]['tab_parent']; $active_sibling = $_SESSION['active-tabs'][$parent]; if (empty($active_sibling)) { return ''; } if ($active_sibling == $clicked_path) { return ''; } $active_nephew = $_SESSION['active-tabs'][$active_sibling]; if (empty($active_nephew)) { return ''; } $active_child = $clicked_path .'/'. _smart_tabs_leaf($active_nephew); if (!isset($tasks[$active_child])) { return ''; } unset($_SESSION['active-tabs'][$active_sibling]); // do we need to do this if we're going to return ''? $new_tab = $tasks[$active_child]; } else { // eg accessible-content for 2nd time, get the child or grandchild $active_grandchild = $_SESSION['active-tabs'][$active_child]; $new_tab = $tasks[empty($active_grandchild) ? $active_child : $active_grandchild]; } if (!$new_tab['access']) { //drupal_set_message(t('Not auto-selecting %tab_name tab at %tab_href as you\'re not authorized to access that page.', array( // '%tab_name' => drupal_ucfirst(_smart_tabs_leaf($new_tab['href'])), // '%tab_href' => _smart_tabs_parent_path($new_tab['href'])))); return ''; } // If the tab that we're about to propose is the default for the clicked_path, // then don't return it, as it will result in an unnecessary additional page // load. $new_active_href = ($new_tab['type'] == MENU_DEFAULT_LOCAL_TASK) ? '' : $new_tab['href']; // Avoid looping back to the already selected tab, eg. clicking 'node/123' // (e.g via menu) when 'node/123/edit' is selected should not take user back // to 'node/123/edit', especially as this causes the associated (primary // or secondary links) menu to collapse, i.e. the menu can't be opened // anymore. // Don't do this for normal local tasks like accessible-content/*/*, as // the second row may fall back to the wrong default. // Also don't do this when 'Smart menus for tabbed content' is ticked. if (($new_active_href == $_SESSION['active-tabs']['last-active-href']) && (strpos($clicked_path, '/%') > 0) && !(module_exists('smart_menus') && _smart_menus_get_user_tabbed_content_expansion())) { return ''; } return $new_active_href; } /** * Store the paths (rather than the hrefs) of the active tabs on this page. * There will be one active tab per row of local tasks, so either one or two on * the page. * * @param $new_active_tabs * @return nothing */ function _smart_tabs_remember_tabs($new_active_tabs) { if (!empty($new_active_tabs)) { foreach ($new_active_tabs as $parent => $child) { $_SESSION['active-tabs'][$parent] = $child; } } } function _smart_tabs_leaf($path) { //$last_slash = strrpos($path, '/'); //return $last_slash === FALSE ? $path : substr($path, $last_slash + 1); return end(explode('/', $path)); } /** * Return the path up to (and excluding) the last slash in the supplied string. * * @param $path * A URL * @return * The substring upto the last slash, or the entire string if it contains no * slash. */ function _smart_tabs_parent_path($path) { return substr($path, 0, strrpos($path, '/')); } function _smart_tabs_generate_link($item, $tasks) { // In order to distinguish the parent menu item from the default tab (e.g. // user/7/edit and user/7/edit/account), it is preferable to not use the // alias for the default tab, as it will stop us from returning to the // default tab (we'll end up at the last selected non-default tab instead). // However for node aliases this is less of an issue, so we can be more // user-friendly and dispaly the alias. // The exception are modules that auto-expand path aliases (subpath_alias and // path_alias_xt), as they again cause the non-default tabs, like // node/123/edit, to appear like a child of the default node/123, thus // causing a redirect whenever the View tab is clicked. // These modules take care of the aliases themselves so no need to interfere. // if ($item['type'] == MENU_DEFAULT_LOCAL_TASK && substr($item['href'], 0, 5) == 'node/' && !module_exists('path_alias_xt')) { // When the tab is the default, replace its href by the href of // the first parent that is NOT a default task, so that alias // substitution can take place inside theme_menu_item_link(). $parent_href = _smart_tabs_get_parent_href($tasks, $item['tab_parent']); $link = theme('menu_item_link', array('href' => $parent_href) + $item); } else { $link = theme('menu_item_link', $item); } return $link; } function _smart_tabs_get_parent_href($tasks, $path) { while ($path && $tasks[$path]['type'] == MENU_DEFAULT_LOCAL_TASK) { $path = $tasks[$path]['tab_parent']; } return $tasks[$path]['href']; } function _st_debug_info($message) { global $user; if ($user->uid == 1 && variable_get('smart_tabs_debug', FALSE)) { drupal_set_message("Smart tabs - $message", 'warning', FALSE); } } /** * Implementation of MODULENAME_preprocess_hook(), where hook==page. * * Use only in cases where an installed (base) theme already implements * phptemplate_menu_local_tasks(), like Garland does, or implements * THEMENAME_menu_local_tasks(), like Zen does. * * @ingroup themeable * function smart_tabs_preprocess_page(&$variables) { { $variables['tabs'] = theme(array('menu_local_tasks_smart_tabs', 'menu_local_tasks')); }*/