Menus are a collection of links (menu items) used to navigate a website. The menu module provides an interface to control and customize the powerful menu system that comes with Drupal. Menus are primarily displayed as a hierarchical list of links using Drupal\'s highly flexible blocks feature. Each menu automatically creates a block of the same name. By default, new menu items are placed inside a built-in menu labelled %navigation, but administrators can also create custom menus.
Drupal themes generally provide out-of-the-box support for two menus commonly labelled %primary-links and %secondary-links. These are sets of links which are usually displayed in the header or footer of each page (depending on the currently active theme).
Menu administration tabs:'. t('For more information please read the configuration and customization handbook Menu page.', array('@menu' => 'http://drupal.org/handbook/modules/menu/')) .'
'; return $output; case 'admin/build/menu': return ''. t('Menus are a collection of links (menu items) used to navigate a website. The list(s) below display the currently available menus along with their menu items. Select an operation from the list to manage each menu or menu item.', array('@admin-settings-menus' => url('admin/build/menu/settings'), '@admin-block' => url('admin/build/block'))) .'
'; case 'admin/build/menu/add': return ''. t('Enter the name for your new menu. Remember to enable the newly created block in the blocks administration page.', array('@blocks' => url('admin/build/block'))) .'
'; case 'admin/build/menu/item/add': return ''. t('Enter the title, path, position and the weight for your new menu item.') .'
'; } } /** * Implementation of hook_perm(). */ function menu_perm() { return array('administer menu'); } /** * Implementation of hook_menu(). */ function menu_menu() { $items['admin/build/menu'] = array( 'title' => 'Menus', 'description' => "Control your site's navigation menu, primary links and secondary links. as well as rename and reorganize menu items.", 'page callback' => 'menu_overview_page', 'access callback' => 'user_access', 'access arguments' => array('administer menu'), ); $items['admin/build/menu/list'] = array( 'title' => 'List menus', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); $items['admin/build/menu/add'] = array( 'title' => 'Add menu', 'page callback' => 'drupal_get_form', 'page arguments' => array('menu_edit_menu', 'add'), 'type' => MENU_LOCAL_TASK); $items['admin/build/menu/settings'] = array( 'title' => 'Settings', 'page callback' => 'drupal_get_form', 'page arguments' => array('menu_configure'), 'type' => MENU_LOCAL_TASK, 'weight' => 5); $items['admin/build/menu-customize/%menu'] = array( 'title' => 'Customize menu', 'page callback' => 'menu_overview', 'page arguments' => array(3), 'title callback' => 'menu_overview_title', 'title arguments' => array(3), 'access arguments' => array('administer menu'), 'type' => MENU_CALLBACK); $items['admin/build/menu-customize/%menu/list'] = array( 'title' => 'List items', 'weight' => -10, 'type' => MENU_DEFAULT_LOCAL_TASK); $items['admin/build/menu-customize/%menu/add'] = array( 'title' => 'Add item', 'page callback' => 'drupal_get_form', 'page arguments' => array('menu_edit_item', 'add', NULL, 3), 'type' => MENU_LOCAL_TASK); $items['admin/build/menu-customize/%menu/edit'] = array( 'title' => 'Edit menu', 'page callback' => 'drupal_get_form', 'page arguments' => array('menu_edit_menu', 'edit', 3), 'type' => MENU_LOCAL_TASK); $items['admin/build/menu-customize/%menu/delete'] = array( 'title' => 'Delete menu', 'page callback' => 'menu_delete_menu_page', 'page arguments' => array(3), 'type' => MENU_CALLBACK); $items['admin/build/menu/item/%menu_link/disable'] = array( 'title' => 'Disable menu item', 'page callback' => 'menu_flip_item', 'page arguments' => array(TRUE, 4), 'type' => MENU_CALLBACK); $items['admin/build/menu/item/%menu_link/enable'] = array( 'title' => 'Enable menu item', 'page callback' => 'menu_flip_item', 'page arguments' => array(FALSE, 4), 'type' => MENU_CALLBACK); $items['admin/build/menu/item/%menu_link/edit'] = array( 'title' => 'Edit menu item', 'page callback' => 'drupal_get_form', 'page arguments' => array('menu_edit_item', 'edit', 4, NULL), 'type' => MENU_CALLBACK); $items['admin/build/menu/item/%menu_link/reset'] = array( 'title' => 'Reset menu item', 'page callback' => 'drupal_get_form', 'page arguments' => array('menu_reset_item_confirm', 4), 'type' => MENU_CALLBACK); $items['admin/build/menu/item/%menu_link/delete'] = array( 'title' => 'Delete menu item', 'page callback' => 'menu_item_delete_page', 'page arguments' => array(4), 'type' => MENU_CALLBACK); return $items; } /** * Implementation of hook_enable() * * Add a link for each custom menu. */ function menu_enable() { menu_rebuild(); $result = db_query("SELECT * FROM {menu_custom}"); $link['module'] = 'menu'; $link['plid'] = db_result(db_query("SELECT mlid from {menu_links} WHERE menu_name = 'navigation' AND link_path = 'admin/build/menu'")); $link['router_path'] = 'admin/build/menu-customize/%'; while ($menu = db_fetch_array($result)) { $link['mlid'] = 0; $link['link_title'] = $menu['title']; $link['link_path'] = 'admin/build/menu-customize/'. $menu['menu_name']; menu_link_save($link); } menu_cache_clear_all(); } /** * Title callback for the menu overview page and links. */ function menu_overview_title($menu) { return t('!menu_title (overview)', array('!menu_title' => $menu['title'])); } /** * Load the data for a single custom menu. */ function menu_load($menu_name) { return db_fetch_array(db_query("SELECT * FROM {menu_custom} WHERE menu_name = '%s'", $menu_name)); } /** * Menu callback which shows an overview page of all the custom menus and their descriptions. */ function menu_overview_page() { $result = db_query("SELECT * FROM {menu_custom} ORDER BY title"); $content = array(); while ($menu = db_fetch_array($result)) { $menu['href'] = 'admin/build/menu-customize/'. $menu['menu_name']; $menu['options'] = array(); $content[] = $menu; } return theme('admin_block_content', $content); } /** * Shows for one menu the menu items accessible to the current user and relevant operations. */ function menu_overview($menu) { $header = array(t('Menu item'), t('Expanded'), array('data' => t('Operations'), 'colspan' => '3')); $sql =" SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, ml.* FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.menu_name = '%s' ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC"; $sql_count = "SELECT COUNT(*) FROM {menu_links} ml WHERE menu_name = '%s'"; $result = pager_query($sql, 200, 0, $sql_count, $menu['menu_name']); $tree = menu_tree_data($result); $node_links = array(); menu_tree_collect_node_links($tree, $node_links); menu_tree_check_access($tree, $node_links); $rows = _menu_overview_tree($tree); $output = theme('table', $header, $rows); $output .= theme('pager', NULL, 200, 0); return $output; } /** * Recursive helper function for menu_overview(). */ function _menu_overview_tree($tree) { static $rows = array(); foreach ($tree as $data) { $title = ''; $item = $data['link']; // Don't show callbacks; these have $item['hidden'] < 0. if ($item && $item['hidden'] >= 0) { $title = str_repeat(' ', $item['depth'] - 1) . ($item['depth'] > 1 ? '- ' : ''); $title .= l($item['title'], $item['href'], $item['options']); // Populate the operations field. $operations = array(); // Set the edit column. $operations[] = array('data' => l(t('edit'), 'admin/build/menu/item/'. $item['mlid'] .'/edit')); if ($item['hidden']) { $title .= ' ('. t('disabled') .')'; $class = 'menu-disabled'; $operations[] = array('data' => l(t('enable'), 'admin/build/menu/item/'. $item['mlid'] .'/enable')); } else { $class = 'menu-enabled'; $operations[] = array('data' => l(t('disable'), 'admin/build/menu/item/'. $item['mlid'] .'/disable')); } // Only items created by the menu module can be deleted. if ($item['module'] == 'menu') { $operations[] = array('data' => l(t('delete'), 'admin/build/menu/item/'. $item['mlid'] .'/delete')); } // Set the reset column. elseif ($item['module'] == 'system' && $item['customized']) { $operations[] = array('data' => l(t('reset'), 'admin/build/menu/item/'. $item['mlid'] .'/reset')); } else { $operations[] = array('data' => ''); } $row = array( array('data' => $title, 'class' => $class), array('data' => ($item['has_children'] ? (($item['expanded']) ? t('Yes') : t('No')) : ''), 'class' => $class), ); foreach ($operations as $operation) { $operation['class'] = $class; $row[] = $operation; } $rows[] = $row; } if ($data['below']) { _menu_overview_tree($data['below']); } } return $rows; } /** * Menu callback; enable/disable a menu link. * * @param $hide * TRUE to not show in the menu tree. FALSE to make the item and its children * reappear in menu tree. * @param $item * The menu item. */ function menu_flip_item($hide, $item) { $item['hidden'] = (bool)$hide; $item['customized'] = 1; menu_link_save($item); drupal_set_message($hide ? t('The menu item has been disabled.') : t('The menu item has been enabled.')); drupal_goto('admin/build/menu-customize/'. $item['menu_name']); } /** * Menu callback; Build the menu link editing form. */ function menu_edit_item(&$form_state, $type, $item, $menu) { if ($type == 'add' || empty($item)) { // This is an add form, initialize the menu link. $item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu['menu_name'], 'weight' => 0, 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0); } foreach (array('link_path', 'mlid', 'module', 'hidden', 'has_children', 'options') as $key) { $form[$key] = array('#type' => 'value', '#value' => $item[$key]); } // Any item created or edited via this interface is considered "customized". $form['customized'] = array('#type' => 'value', '#value' => 1); $form['original_item'] = array('#type' => 'value', '#value' => $item); if ($item['module'] == 'menu') { $form['link_path'] = array( '#type' => 'textfield', '#title' => t('Path'), '#default_value' => $item['link_path'], '#description' => t('The path this menu item links to. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => ''. format_plural($num_links, 'Warning: There is currently 1 menu item in %title. It will be deleted (system-defined items will be reset).', 'Warning: There are currently @count menu items in %title. They will be deleted (system-defined items will be reset).', array('%title' => $menu['title'])) .'
'; } $caption .= ''. t('This action cannot be undone.') .'
'; return confirm_form($form, t('Are you sure you want to delete the custom menu %title?', array('%title' => $menu['title'])), 'admin/build/menu-customize/'. $menu['menu_name'], $caption, t('Delete')); } /** * Delete a custom menu and all items in it. */ function menu_delete_menu_confirm_submit($form, &$form_state) { $menu = $form['#menu']; $form_state['redirect'] = 'admin/build/menu'; // System-defined menus may not be deleted - only menus defined by this module. if (in_array($menu['menu_name'], menu_list_system_menus()) || !db_result(db_query("SELECT COUNT(*) FROM {menu_custom} WHERE menu_name = '%s'", $menu['menu_name']))) { return; } // Reset all the menu links defined by the system via hook_menu. $result = db_query("SELECT * FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = '%s' AND ml.module = 'system' ORDER BY m.number_parts ASC", $menu['menu_name']); while ($item = db_fetch_array($result)) { menu_reset_item($item); } // Delete all links to the overview page for this menu. $result = db_query("SELECT mlid FROM {menu_links} ml WHERE ml.link_path = '%s'", 'admin/build/menu-customize/'. $menu['menu_name']); while ($m = db_fetch_array($result)) { menu_link_delete($m['mlid']); } // Delete all the links in the menu and the menu from the list of custom menus. db_query("DELETE FROM {menu_links} WHERE menu_name = '%s'", $menu['menu_name']); db_query("DELETE FROM {menu_custom} WHERE menu_name = '%s'", $menu['menu_name']); // Delete all the blocks for this menu. db_query("DELETE FROM {blocks} WHERE module = 'menu' AND delta = '%s'", $menu['menu_name']); db_query("DELETE FROM {blocks_roles} WHERE module = 'menu' AND delta = '%s'", $menu['menu_name']); menu_cache_clear_all(); cache_clear_all(); $t_args = array('%title' => $menu['title']); drupal_set_message(t('The custom menu %title has been deleted.', $t_args)); watchdog('menu', 'Deleted custom menu %title and all its menu items.', $t_args, WATCHDOG_NOTICE); } /** * Validates the human and machine-readable names when adding or editing a menu. */ function menu_edit_menu_validate($form, &$form_state) { $item = $form_state['values']; if (preg_match('/[^a-z0-9-]/', $item['menu_name'])) { form_set_error('menu_name', t('Menu name may consist only of lowercase letters, numbers, and hyphens.')); } if ($form['#insert']) { // We will add 'menu-' to the menu name to help avoid name-space conflicts. $item['menu_name'] = 'menu-'. $item['menu_name']; if (db_result(db_query("SELECT menu_name FROM {menu_custom} WHERE menu_name = '%s'", $item['menu_name'])) || db_result(db_query_range("SELECT menu_name FROM {menu_links} WHERE menu_name = '%s'", $item['menu_name'], 0, 1))) { form_set_error('menu_name', t('Menu already exists')); } } } /** * Submit function for adding or editing a custom menu. */ function menu_edit_menu_submit($form, &$form_state) { $menu = $form_state['values']; $path = 'admin/build/menu-customize/'; if ($form['#insert']) { // Add 'menu-' to the menu name to help avoid name-space conflicts. $menu['menu_name'] = 'menu-'. $menu['menu_name']; $link['link_title'] = $menu['title']; $link['link_path'] = $path . $menu['menu_name']; $link['router_path'] = $path .'%'; $link['module'] = 'menu'; $link['plid'] = db_result(db_query("SELECT mlid from {menu_links} WHERE menu_name = 'navigation' AND link_path = 'admin/build/menu'")); menu_link_save($link); db_query("INSERT INTO {menu_custom} (menu_name, title, description) VALUES ('%s', '%s', '%s')", $menu['menu_name'], $menu['title'], $menu['description']); } else { db_query("UPDATE {menu_custom} SET title = '%s', description = '%s' WHERE menu_name = '%s'", $menu['title'], $menu['description'], $menu['menu_name']); $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = '%s'", $path . $menu['menu_name']); while ($m = db_fetch_array($result)) { $link = menu_link_load($m['mlid']); $link['link_title'] = $menu['title']; menu_link_save($link); } } $form_state['redirect'] = $path . $menu['menu_name']; } /** * Menu callback; Check access and present a confirm form for deleting a menu link. */ function menu_item_delete_page($item) { // Links defined via hook_menu may not be deleted. if ($item['module'] == 'system') { drupal_access_denied(); return; } return drupal_get_form('menu_item_delete_form', $item); } /** * Build a confirm form for deletion of a single menu link. */ function menu_item_delete_form(&$form_state, $item) { $form['#item'] = $item; return confirm_form($form, t('Are you sure you want to delete the custom menu item %item?', array('%item' => $item['link_title'])), 'admin/build/menu-customize/'. $item['menu_name']); } /** * Process menu delete form submissions. */ function menu_item_delete_form_submit($form, &$form_state) { $item = $form['#item']; menu_link_delete($item['mlid']); $t_args = array('%title' => $item['link_title']); drupal_set_message(t('The menu item %title has been deleted.', $t_args)); watchdog('menu', 'Deleted menu item %title.', $t_args, WATCHDOG_NOTICE); $form_state['redirect'] = 'admin/build/menu-customize/'. $item['menu_name']; } /** * Menu callback; reset a single modified item. */ function menu_reset_item_confirm(&$form_state, $item) { $form['item'] = array('#type' => 'value', '#value' => $item); return confirm_form($form, t('Are you sure you want to reset the item %item to its default values?', array('%item' => $item['link_title'])), 'admin/build/menu-customize/'. $item['menu_name'], t('Any customizations will be lost. This action cannot be undone.'), t('Reset')); } /** * Process menu reset item form submissions. */ function menu_reset_item_confirm_submit($form, &$form_state) { $item = $form_state['values']['item']; $new_item = menu_reset_item($item); drupal_set_message(t('The menu item was reset to its default settings.')); $form_state['redirect'] = 'admin/build/menu-customize/'. $new_item['menu_name']; } /** * Reset a system-defined menu item. */ function menu_reset_item($item) { $router = menu_router_build(); $new_item = _menu_link_build($router[$item['router_path']]); foreach (array('mlid', 'has_children') as $key) { $new_item[$key] = $item[$key]; } menu_link_save($new_item); return $new_item; } /** * Implementation of hook_block(). */ function menu_block($op = 'list', $delta = 0) { $custom_menus = menu_get_menus(FALSE); if ($op == 'list') { $blocks = array(); foreach ($custom_menus as $name => $title) { // Default "Navigation" block is handled by user.module. $blocks[$name]['info'] = check_plain($title); } return $blocks; } else if ($op == 'view') { $data['subject'] = check_plain($custom_menus[$delta]); $data['content'] = menu_tree($delta); return $data; } } /** * Implementation of hook_nodeapi(). */ function menu_nodeapi(&$node, $op) { switch ($op) { case 'insert': case 'update': if (isset($node->menu)) { $item = $node->menu; if (!empty($item['delete'])) { menu_link_delete($item['mlid']); } elseif (trim($item['link_title'])) { $item['link_title'] = trim($item['link_title']); $item['link_path'] = "node/$node->nid"; if (!$item['customized']) { $item['options']['attributes']['title'] = trim($node->title); } if (!menu_link_save($item)) { drupal_set_message(t('There was an error saving the menu link.'), 'error'); } } } break; case 'delete': // Delete all menu module links that point to this node. $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu'", $node->nid); while ($m = db_fetch_array($result)) { menu_link_delete($m['mlid']); } break; case 'prepare': if (empty($node->menu)) { // Prepare the node for the edit form so that $node->menu always exists. $menu_name = variable_get('menu_default_node_menu', 'navigation'); $item = array(); if (isset($node->nid)) { // Give priority to the default menu $mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND menu_name = '%s' AND module = 'menu' ORDER BY mlid ASC", $node->nid, $menu_name, 0, 1)); // Check all menus if a link does not exist in the default menu. if (!$mlid) { $mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu' ORDER BY mlid ASC", $node->nid, 0, 1)); } if ($mlid) { $item = menu_link_load($mlid); } } // Set default values. $node->menu = $item + array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu_name, 'weight' => 0, 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0, 'customized' => 0); } break; } } /** * Implementation of hook_form_alter(). Adds menu item fields to the node form. */ function menu_form_alter(&$form, $form_state, $form_id) { if (isset($form['#node']) && $form['#node']->type .'_node_form' == $form_id) { // Note - doing this to make sure the delete checkbox stays in the form. $form['#cache'] = TRUE; $form['menu'] = array( '#type' => 'fieldset', '#title' => t('Menu settings'), '#access' => user_access('administer menu'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#tree' => TRUE, '#weight' => -2, ); $item = $form['#node']->menu; if ($item['mlid']) { // There is an existing link. $form['menu']['delete'] = array( '#type' => 'checkbox', '#title' => t('Check to remove this item from the menu.'), ); } if (!$item['link_title']) { $form['menu']['#collapsed'] = TRUE; } foreach (array('mlid', 'module', 'hidden', 'has_children', 'customized', 'options', 'expanded', 'hidden') as $key) { $form['menu'][$key] = array('#type' => 'value', '#value' => $item[$key]); } $form['menu']['link_title'] = array('#type' => 'textfield', '#title' => t('Menu link title'), '#default_value' => $item['link_title'], '#description' => t('The link text corresponding to this item that should appear in the menu. Leave blank if you do not wish to add this post to the menu.'), '#required' => FALSE, ); // Generate a list of possible parents (not including this item or descendants). $options = menu_parent_options(menu_get_menus(), $item); $default = $item['menu_name'] .':'. $item['plid']; if (!isset($options[$default])) { $default = 'navigation:0'; } $form['menu']['parent'] = array( '#type' => 'select', '#title' => t('Parent item'), '#default_value' => $default, '#options' => $options, ); $form['#submit'][] = 'menu_node_form_submit'; $form['menu']['weight'] = array( '#type' => 'weight', '#title' => t('Weight'), '#default_value' => $item['weight'], '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'), ); } } /** * Decompose the selected menu parent option into the menu_name and plid. */ function menu_node_form_submit($form, &$form_state) { list($form_state['values']['menu']['menu_name'], $form_state['values']['menu']['plid']) = explode(':', $form_state['values']['menu']['parent']); } /** * Return an associative array of the custom menus names. * * @param $all * If FALSE return only user-added menus, or if TRUE also include * the menus defined by the system. * @return * An array with the machine-readable names as the keys, and human-readable * titles as the values. */ function menu_get_menus($all = TRUE) { $system_menus = menu_list_system_menus(); $sql = 'SELECT * FROM {menu_custom}'; if (!$all) { $sql .= ' WHERE menu_name NOT IN ('. implode(',', array_fill(0, count($system_menus), "'%s'")) .')'; } $sql .= ' ORDER BY title'; $result = db_query($sql, $system_menus); $rows = array(); while ($r = db_fetch_array($result)) { $rows[$r['menu_name']] = $r['title']; } return $rows; } /** * Menu callback; Build the form presenting menu configuration options. */ function menu_configure() { $form['intro'] = array( '#type' => 'item', '#value' => t('The menu module allows on-the-fly creation of menu links in the content authoring forms. The following option sets the default menu in which a new link will be added.'), ); $authoring_options = menu_get_menus(); $form['menu_default_node_menu'] = array('#type' => 'select', '#title' => t('Default menu for content'), '#default_value' => variable_get('menu_default_node_menu', 'navigation'), '#options' => $authoring_options, '#description' => t('Choose the menu to be the default in the menu options in the content authoring form.'), ); return system_settings_form($form); } /** * Validates the path of a menu link being created or edited. * * @return * TRUE if it is a valid path AND the current user has access permission, * FALSE otherwise. */ function menu_valid_path($form_item) { $item = array(); $path = $form_item['link_path']; if ($path == '