Commit 10bdb51c authored by Dries's avatar Dries

- Patch by JonBob/Jonathan: reworked the menu system so that menus are

  configurable!  Menu items can be disabled, repositioned, added and
  so on.

  Upgrading to requires you to run update.php.

  This functionality depricates some of the 'navigation modules' in the
  contributions repository.  Furthermore, modules can now 'suggest'
  menu items and site adminstrators can choose to enable them.  Modules
  in the contributions repository should try to take advantage of this.
parent a083daf8
......@@ -2,7 +2,7 @@ Drupal x.x.x, xxxx-xx-xx
------------------------
- profile module:
* made it possible to administere profile fields.
* made it possible to administer profile fields.
* made it possible to browse the profiles by field.
Drupal 4.4.0, 2004-04-01 (release candidate)
......
......@@ -235,6 +235,21 @@ CREATE TABLE locales (
PRIMARY KEY (lid)
) TYPE=MyISAM;
--
-- Table structure for table 'menu'
--
CREATE TABLE menu (
mid int(10) unsigned NOT NULL default '0',
pid int(10) unsigned NOT NULL default '0',
path varchar(255) NOT NULL default '',
title varchar(255) NOT NULL default '',
weight tinyint(4) NOT NULL default '0',
visibility int(1) unsigned NOT NULL default '0',
status int(1) unsigned NOT NULL default '0',
PRIMARY KEY (mid)
) TYPE=MyISAM;
--
-- Table structure for table 'moderation_filters'
--
......@@ -632,3 +647,4 @@ REPLACE variable SET name='theme_default', value='s:9:"xtemplate";';
REPLACE blocks SET module = 'user', delta = '0', status = '1';
REPLACE blocks SET module = 'user', delta = '1', status = '1';
INSERT INTO sequences (name, id) VALUES ('menu_mid', 1);
......@@ -53,7 +53,8 @@
"2004-02-21" => "update_79",
"2004-03-11: first update since Drupal 4.4.0 release" => "update_80",
"2004-02-20" => "update_81",
"2004-02-27" => "update_82"
"2004-02-27" => "update_82",
"2004-04-15" => "update_83"
);
function update_32() {
......@@ -900,6 +901,27 @@ function update_82() {
return $ret;
}
function update_83() {
$ret = array();
if ($GLOBALS["db_type"] == "mysql") {
$ret[] = update_sql("CREATE TABLE menu (
mid int(10) unsigned NOT NULL default '0',
pid int(10) unsigned NOT NULL default '0',
path varchar(255) NOT NULL default '',
title varchar(255) NOT NULL default '',
weight tinyint(4) NOT NULL default '0',
visibility int(1) unsigned NOT NULL default '0',
status int(1) unsigned NOT NULL default '0',
PRIMARY KEY (mid)
);");
$ret[] = update_sql("INSERT INTO sequences (name, id) VALUES ('menu_mid', 1)");
}
else {
/* Needs PostgreSQL equivalent */
}
return $ret;
}
function update_sql($sql) {
$edit = $_POST["edit"];
$result = db_query($sql);
......
......@@ -10,6 +10,11 @@
define('MENU_HIDE', 1);
define('MENU_HIDE_NOCHILD', 2);
define('MENU_NORMAL', 0);
define('MENU_MODIFIED', 1);
define('MENU_LOCKED', 2);
define('MENU_CUSTOM', 3);
/** @} */
/**
......@@ -20,39 +25,82 @@
* @param $title The title of the menu item to show in the rendered menu.
* @param $callback The function to call when this is the active menu item.
* @param $weight Heavier menu items sink down the menu.
* @param $hidden
* - MENU_SHOW show the menu (default).
* - MENU_HIDE hide the menu item, but register a callback.
* - MENU_HIDE_NOCHILD hide the menu item when it has no children.
* @param $visibility
* - MENU_SHOW - Show the menu item (default).
* - MENU_HIDE - Hide the menu item, but register a callback.
* - MENU_HIDE_NOCHILD - Hide the menu item when it has no children.
* @param $status
* - MENU_NORMAL - The menu item can be moved (default).
* - MENU_MODIFIED - The administrator has moved or otherwise changed the menu item.
* - MENU_LOCKED - The administrator may not modify the item.
* - MENU_CUSTOM - The menu item was created by the administrator.
*/
function menu($path, $title, $callback = NULL, $weight = 0, $hidden = MENU_SHOW) {
global $_list;
function menu($path, $title, $callback = NULL, $weight = 0, $visibility = MENU_SHOW, $status = MENU_NORMAL) {
global $_menu;
// add the menu to the flat list of menu items:
$_list[$path] = array("title" => $title, "callback" => $callback, "weight" => $weight, "hidden" => $hidden);
$_menu['list'][$path] = array('title' => $title, 'callback' => $callback, 'weight' => $weight, 'visibility' => $visibility, 'status' => $status);
}
/**
* Return the menu data structure.
*
* The returned structure contains much information that is useful only
* internally in the menu system. External modules are likely to need only
* the ['visible'] element of the returned array. All menu items that are
* accessible to the current user and not hidden will be present here, so
* modules and themes can use this structure to build their own representations
* of the menu.
*
* $menu['visible'] will contain an associative array, the keys of which
* are menu IDs. The values of this array are themselves associative arrays,
* with the following key-value pairs defined:
* - 'title' - The displayed title of the menu or menu item. It will already
* have been translated by the locale system.
* - 'path' - The Drupal path to the menu item. A link to a particular item
* can thus be constructed with l($item['title'], $item['path']).
* - 'children' - A linear list of the menu ID's of this item's children.
*
* Menu ID 0 is the "root" of the menu. The children of this item are the
* menus themselves (they will have no associated path). Menu ID 1 will
* always be one of these children; it is the default "Navigation" menu.
*/
function menu_get_menu() {
global $_menu;
global $user;
if (!isset($_menu['items'])) {
$cache = cache_get('menu:'. $user->uid);
if ($cache) {
$_menu = unserialize($cache->data);
}
else {
menu_build();
cache_set('menu:'. $user->uid, serialize($_menu), 1);
}
}
return $_menu;
}
/**
* Returns an array with the menu items that lead to the specified path.
*/
function menu_get_trail($path) {
global $_list;
$menu = menu_get_menu();
$trail = array();
while ($path) {
if ($_list[$path]) {
array_unshift($trail, $path);
}
$path = substr($path, 0, strrpos($path, "/"));
$mid = menu_get_active_item();
while ($mid && $menu['items'][$mid]) {
array_unshift($trail, $mid);
$mid = $menu['items'][$mid]['pid'];
}
return $trail;
}
/**
* Returns the path of the active menu item.
* Returns the ID of the active menu item.
* @ingroup menu
*/
function menu_get_active_item() {
......@@ -64,34 +112,34 @@ function menu_get_active_item() {
* @ingroup menu
*/
function menu_set_active_item($path = NULL) {
global $_list;
static $stored_path;
static $stored_mid;
$menu = menu_get_menu();
if (is_null($stored_path) || !empty($path)) {
if (is_null($stored_mid) || !empty($path)) {
if (empty($path)) {
$path = $_GET["q"];
$path = $_GET['q'];
}
else {
$_GET['q'] = $path;
}
while ($path && !$_list[$path]) {
$path = substr($path, 0, strrpos($path, "/"));
while ($path && !$menu['path index'][$path]) {
$path = substr($path, 0, strrpos($path, '/'));
}
$stored_path = $path;
$stored_mid = $menu['path index'][$path];
}
return $stored_path;
return $stored_mid;
}
/**
* Returns the title of the active menu item.
*/
function menu_get_active_title() {
global $_list;
$menu = menu_get_menu();
if ($path = menu_get_active_item()) {
return ucfirst($_list[$path]["title"]);
if ($mid = menu_get_active_item()) {
return ucfirst($menu['items'][$mid]['title']);
}
}
......@@ -101,10 +149,10 @@ function menu_get_active_title() {
function menu_get_active_help() {
if (menu_active_handler_exists()) {
$path = $_GET["q"];
$output = "";
$path = $_GET['q'];
$output = '';
$return = module_invoke_all("help", $path);
$return = module_invoke_all('help', $path);
foreach ($return as $item) {
if (!empty($item)) {
$output .= $item ."\n";
......@@ -118,168 +166,282 @@ function menu_get_active_help() {
* Returns an array of rendered menu items in the active breadcrumb trail.
*/
function menu_get_active_breadcrumb() {
$menu = menu_get_menu();
$links[] = l(t("Home"), "");
$links[] = l(t('Home'), '');
$trail = menu_get_trail($_GET["q"]);
foreach ($trail as $item) {
$links[] = _render_item($item);
$trail = menu_get_trail($_GET['q']);
foreach ($trail as $mid) {
// Don't show menu items without valid link targets.
if ($menu['items'][$mid]['path'] != '') {
$links[] = _menu_render_item($mid);
}
}
return $links;
}
/**
* Execute the handler associated with the active menu item.
*/
function menu_execute_active_handler() {
global $_list;
$menu = menu_get_menu();
$path = menu_get_active_item();
$path = $_GET['q'];
while ($path && (!$menu['path index'][$path] || !$menu['items'][$menu['path index'][$path]]['callback'])) {
$path = substr($path, 0, strrpos($path, '/'));
}
$mid = $menu['path index'][$path];
if ($_list[$path]["callback"]) {
$arg = substr($_GET["q"], strlen($path) + 1);
if ($menu['items'][$mid]['callback']) {
$arg = substr($_GET['q'], strlen($menu['items'][$mid]['path']) + 1);
if (isset($arg)) {
return call_user_func_array($_list[$path]["callback"], explode("/", $arg));
return call_user_func_array($menu['items'][$mid]['callback'], explode('/', $arg));
}
else {
return call_user_func($_list[$path]["callback"]);
return call_user_func($menu['items'][$mid]['callback']);
}
}
}
/**
* Return true if a valid callback can be called from the current path.
*/
function menu_active_handler_exists() {
global $_list;
$menu = menu_get_menu();
$path = menu_get_active_item();
$path = $_GET['q'];
while ($path && (!$menu['path index'][$path] || !$menu['items'][$menu['path index'][$path]]['callback'])) {
$path = substr($path, 0, strrpos($path, '/'));
}
$mid = $menu['path index'][$path];
return function_exists($_list[$path]["callback"]);
return function_exists($menu['items'][$mid]['callback']);
}
/**
* Returns true when the path is in the active trail.
*/
function menu_in_active_trail($path) {
function menu_in_active_trail($mid) {
static $trail;
if (empty($trail)) {
$trail = menu_get_trail($_GET["q"]);
$trail = menu_get_trail($_GET['q']);
}
return in_array($path, $trail);
return in_array($mid, $trail);
}
/**
* Returns true when the menu has visisble children.
* Returns a rendered menu tree.
*/
function menu_has_visible_children($item) {
global $_list;
function menu_tree($pid = 1) {
static $trail;
$menu = menu_get_menu();
$output = '';
if ($_list[$item]['children']) {
foreach ($_list[$item]['children'] as $child) {
if ($_list[$child]['hidden'] == MENU_SHOW) {
return true;
if (empty($trail)) {
$trail = menu_get_trail($_GET['q']);
}
if (isset($menu['visible'][$pid]) && $menu['visible'][$pid]['children']) {
foreach ($menu['visible'][$pid]['children'] as $mid) {
$style = (count($menu['visible'][$mid]['children']) ? (menu_in_active_trail($mid) ? 'expanded' : 'collapsed') : 'leaf');
$output .= "<li class=\"$style\">";
$output .= _menu_render_item($mid);
if (menu_in_active_trail($mid)) {
$output .= menu_tree($mid);
}
$output .= "</li>\n";
}
if ($output != '') {
$output = "\n<ul>\n$output\n</ul>\n";
}
}
return false;
return $output;
}
/**
* Returns a rendered menu tree.
* Build the menu by querying both modules and the database.
*/
function menu_tree($parent = "", $hidden = 0) {
global $_list;
static $trail;
$output = "";
function menu_build() {
global $_menu;
global $user;
if (empty($trail)) {
$trail = menu_get_trail($_GET["q"]);
// Start from a clean slate.
$_menu = array();
// Build a sequential list of all menu items.
module_invoke_all('link', 'system');
$_menu['path index'] = array();
// Set up items array, including default "Navigation" menu.
$_menu['items'] = array(0 => array(), 1 => array('pid' => 0, 'title' => t('Navigation'), 'weight' => -50, 'visibility' => MENU_SHOW, 'status' => MENU_LOCKED));
// Menu items not in the DB get temporary negative IDs.
$temp_mid = -1;
foreach ($_menu['list'] as $path => $data) {
$mid = $temp_mid;
$_menu['items'][$mid] = array('path' => $path, 'title' => $data['title'], 'callback' => $data['callback'], 'weight' => $data['weight'], 'visibility' => $data['visibility'], 'status' => $data['status']);
$_menu['path index'][$path] = $mid;
$temp_mid--;
}
if (isset($_list[$parent]) && $_list[$parent]["children"]) {
usort($_list[$parent]["children"], "_menu_sort");
foreach ($_list[$parent]["children"] as $item) {
/*
** Don't render the menu when it is hidden, or when it has no call-back
** nor children. The latter check avoids that useless links are being
** rendered.
*/
$visible = menu_has_visible_children($item);
if (($_list[$item]["hidden"] == MENU_SHOW && $_list[$item]["callback"]) ||
($_list[$item]["hidden"] == MENU_SHOW && $visible) ||
($_list[$item]["hidden"] == MENU_HIDE_NOCHILD && $visible)) {
$style = ($visible ? (menu_in_active_trail($item) ? "expanded" : "collapsed") : "leaf");
$output .= "<li class=\"$style\">";
$output .= _render_item($item);
if (menu_in_active_trail($item)) {
$output .= menu_tree($item);
// Now fetch items from the DB, reassigning menu IDs as needed.
if (module_exist('menu')) {
$result = db_query('SELECT * FROM {menu}');
while ($item = db_fetch_object($result)) {
// First, add any custom items added by the administrator.
if ($item->status == MENU_CUSTOM) {
$_menu['items'][$item->mid] = array('pid' => $item->pid, 'path' => $item->path, 'title' => $item->title, 'callback' => NULL, 'weight' => $item->weight, 'visibility' => MENU_SHOW, 'status' => MENU_CUSTOM);
$_menu['path index'][$item->path] = $item->mid;
}
// Don't display non-custom menu items if no module declared them.
else if ($old_mid = $_menu['path index'][$item->path]) {
$_menu['items'][$item->mid] = $_menu['items'][$old_mid];
unset($_menu['items'][$old_mid]);
$_menu['path index'][$item->path] = $item->mid;
// If administrator has changed item position, reflect the change.
if ($item->status == MENU_MODIFIED) {
$_menu['items'][$item->mid]['title'] = $item->title;
$_menu['items'][$item->mid]['pid'] = $item->pid;
$_menu['items'][$item->mid]['weight'] = $item->weight;
$_menu['items'][$item->mid]['visibility'] = $item->visibility;
$_menu['items'][$item->mid]['status'] = $item->status;
}
$output .= "</li>\n";
}
else if ($_list[$item]["hidden"] == MENU_HIDE && $_list[$item]["children"]) {
$output .= menu_tree($item, 1);
}
}
// Establish parent-child relationships.
foreach ($_menu['items'] as $mid => $item) {
if (!isset($item['pid'])) {
// Parent's location has not been customized, so figure it out using the path.
$parent = $item['path'];
do {
$parent = substr($parent, 0, strrpos($parent, '/'));
}
while ($parent && !$_menu['path index'][$parent]);
$pid = $parent ? $_menu['path index'][$parent] : 1;
$_menu['items'][$mid]['pid'] = $pid;
}
else {
$pid = $item['pid'];
}
if ($output != '' && $hidden != MENU_HIDE) {
$output = "\n<ul>\n$output\n</ul>\n";
// Don't make root a child of itself.
if ($mid) {
if (isset ($_menu['items'][$pid])) {
$_menu['items'][$pid]['children'][] = $mid;
}
else {
// If parent is missing, it is a menu item that used to be defined
// but is no longer. Default to a root-level "Navigation" menu item.
$_menu['items'][1]['children'][] = $mid;
}
}
}
return $output;
// Prepare to display trees to the user as required.
menu_build_visible_tree();
}
/**
* Query to module to build the menu.
* Find all visible items in the menu tree, for ease in displaying to user.
*
* Since this is only for display, we only need title, path, and children
* for each item.
*/
function menu_build($type) {
/*
** Build a sequential list of all menus items.
*/
function menu_build_visible_tree($pid = 0) {
global $_menu;
module_invoke_all("link", $type);
if (isset($_menu['items'][$pid])) {
$parent = $_menu['items'][$pid];
/*
** Tree-ify the sequential list of menu items by adding each
** menu item to the 'children' array of their direct parent.
*/
$children = array();
if ($parent['children']) {
usort($parent['children'], '_menu_sort');
foreach ($parent['children'] as $mid) {
$children = array_merge($children, menu_build_visible_tree($mid));
}
}
if (($parent['visibility'] == MENU_SHOW) ||
($parent['visibility'] == MENU_HIDE_NOCHILD && count($children) > 1)) {
$_menu['visible'][$pid] = array('title' => $parent['title'], 'path' => $parent['path'], 'children' => $children);
return array($pid);
}
else {
return $children;
}
}
global $_list;
return array();
}
foreach ($_list as $path => $data) {
/**
* Populate the database representation of the menu.
*
* This need only be called at the start of pages that modify the menu.
*/
function menu_rebuild() {
cache_clear_all();
menu_build();
$menu = menu_get_menu();
$new_items = array();
foreach ($menu['items'] as $mid => $item) {
if ($mid < 0 && ($item->status != MENU_LOCKED)) {
$new_mid = db_next_id('menu_mid');
if (isset($new_items[$item['pid']])) {
$new_pid = $new_items[$item['pid']]['mid'];
}
else {
$new_pid = $item['pid'];
}
/*
** Find $path's direct parent:
*/
$parent = $path;
do {
$parent = substr($parent, 0, strrpos($parent, "/"));
}
while ($parent && !$_list[$parent]);
// Fix parent IDs for menu items already added.
if ($item['children']) {
foreach ($item['children'] as $child) {
if (isset($new_items[$child])) {
$new_items[$child]['pid'] = $new_mid;
}
}
}
if ($path) {
$_list[$parent]["children"][] = $path;
$new_items[$mid] = array('mid' => $new_mid, 'pid' => $new_pid, 'path' => $item['path'], 'title' => $item['title'], 'weight' => $item['weight'], 'visibility' => $item['visibility'], 'status' => $item['status']);
}
}
foreach ($new_items as $item) {
db_query('INSERT INTO {menu} (mid, pid, path, title, weight, visibility, status) VALUES (%d, %d, \'%s\', \'%s\', %d, %d, %d)', $item['mid'], $item['pid'], $item['path'], $item['title'], $item['weight'], $item['visibility'], $item['status']);
}
// Rebuild the menu to account for any changes.
menu_build();
}
/**
* Comparator routine for use in sorting menu items.
*/
function _menu_sort($a, $b) {
global $_list;
$menu = menu_get_menu();
$a = &$_list[$a];
$b = &$_list[$b];
$a = &$menu['items'][$a];
$b = &$menu['items'][$b];
return $a["weight"] < $b["weight"] ? -1 : ($a["weight"] > $b["weight"] ? 1 : ($a["title"] < $b["title"] ? -1 : 1));
return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : ($a['title'] < $b['title'] ? -1 : 1));
}
function _render_item($path) {
global $_list;
function _menu_render_item($mid) {
$menu = menu_get_menu();
return l($_list[$path]["title"], $path);
return l($menu['items'][$mid]['title'], $menu['items'][$mid]['path']<