diff --git admin_menu.api.php admin_menu.api.php index 4ca7a65..44a5a1e 100644 --- admin_menu.api.php +++ admin_menu.api.php @@ -7,6 +7,42 @@ */ /** + * Provide expansion arguments for dynamic menu items, i.e. menu paths + * containing one or more %placeholders. + * + * The map items must be keyed by the dynamic path to expand. Each map item may + * have the following properties: + * + * - parent: The parent menu path to link the expanded items to. + * - arguments: An array of argument sets that will be used in the + * expansion. Each set consists of an array of one or more placeholders with + * an array of possible expansions as value. Upon expansion, each argument + * is combined with every other argument from the set (ie., the cartesian + * product of all arguments is created). The expansion values may be empty, + * that is, you don't need to insert logic to skip map items for which no + * values exist, since admin menu will take care of that. + * - hide: (optional) Used to hide another menu item, usually a superfluous + * "List" item. + * + * @see admin_menu.map.inc + */ +function hook_admin_menu_map() { + // Expand content types in Structure >> Content types. + // The key denotes the dynamic path to expand to multiple menu items. + $map['admin/structure/types/manage/%node_type'] = array( + // Link generated items directly to the "Content types" item, and hide the + // "List" item. + 'parent' => 'admin/structure/types', + 'hide' => 'admin/structure/types/list', + // Create expansion arguments for the %node_type placeholder. + 'arguments' => array( + array('%node_type' => array_keys(node_type_get_types())), + ), + ); + return $map; +} + +/** * Alter content in Administration menu bar before it is rendered. * * @param $content diff --git admin_menu.inc admin_menu.inc index 02320b1..72736bd 100644 --- admin_menu.inc +++ admin_menu.inc @@ -7,6 +7,327 @@ */ /** + * Build the full administration menu tree from static and expanded dynamic items. + * + * @param $menu_name + * The menu name to use as base for the tree. + */ +function admin_menu_tree($menu_name) { + // Get placeholder expansion arguments from hook_admin_menu_map() + // implementations. + module_load_include('inc', 'admin_menu', 'admin_menu.map'); + $expand_map = module_invoke_all('admin_menu_map'); + // Allow modules to alter the expansion map. + drupal_alter('admin_menu_map', $expand_map); + + $new_map = array(); + $hidden = array(); + foreach ($expand_map as $path => $data) { + // Convert named placeholders to anonymous placeholders, since the menu + // system stores paths using anonymous placeholders. + // @todo Why specify named placeholders in the first place then? + $replacements = array_fill_keys(array_keys($data['arguments'][0]), '%'); + $data['parent'] = strtr($data['parent'], $replacements); + $new_map[strtr($path, $replacements)] = $data; + + // Collect paths to hide. + if (isset($data['hide'])) { + $hidden[strtr($data['hide'], $replacements)] = 1; + } + } + $expand_map = $new_map; + unset($new_map); + + // Retrieve dynamic menu link tree for the expansion mappings. + $tree_dynamic = admin_menu_tree_dynamic($expand_map); + + // Merge local tasks with static menu tree. + $tree = menu_tree_all_data($menu_name); + admin_menu_merge_tree($tree, $tree_dynamic, array(), $hidden); + + return $tree; +} + +/** + * Load menu link trees for router paths containing dynamic arguments. + * + * @param $expand_map + * An array containing menu router path placeholder expansion argument + * mappings. + * + * @return + * An associative array whose keys are the parent paths of the menu router + * paths given in $expand_map as well as the parent paths of any child link + * deeper down the tree. The parent paths are used in admin_menu_merge_tree() + * to check whether anything needs to be merged. + * + * @see hook_admin_menu_map() + */ +function admin_menu_tree_dynamic(array $expand_map) { + $p_columns = array(); + for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { + $p_columns[] = 'p' . $i; + } + + // Fetch p* columns for all router paths to expand. + $router_paths = array_keys($expand_map); + $plids = db_select('menu_links', 'ml') + ->fields('ml', $p_columns) + ->condition('router_path', $router_paths) + ->execute() + ->fetchAll(PDO::FETCH_ASSOC); + + // Unlikely, but possible. + if (empty($plids)) { + return array(); + } + + // Use queried plid columns to query sub-trees for the router paths. + $query = db_select('menu_links', 'ml'); + $query->join('menu_router', 'm', 'ml.router_path = m.path'); + $query + ->fields('ml') + ->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), drupal_schema_fields_sql('menu_links'))); + + // The retrieved menu link trees have to be ordered by depth, so parents + // always come before their children for the storage logic below. + // @todo Order by 'fit' or 'depth' instead for parent path handling below? + foreach ($p_columns as $column) { + $query->orderBy($column, 'ASC'); + } + + $db_or = db_or(); + foreach ($plids as $path_plids) { + $db_and = db_and(); + // Skip 0 (zero) columns. + foreach (array_filter($path_plids) as $column => $plid) { + $db_and->condition($column, $plid); + } + $db_or->condition($db_and); + } + $query->condition($db_or); + $result = $query + ->execute() + ->fetchAllAssoc('mlid', PDO::FETCH_ASSOC); + + // Store dynamic links grouped by parent path for later merging and assign + // placeholder expansion arguments. + $tree_dynamic = array(); + foreach ($result as $mlid => $link) { + // If contained in $expand_map, then this is a (first) parent, and we need + // to store by the defined 'parent' path for later merging, as well as + // provide the expansion map arguments to apply to the dynamic tree. + if (isset($expand_map[$link['path']])) { + $parent_path = $expand_map[$link['path']]['parent']; + $link['expand_map'] = $expand_map[$link['path']]['arguments']; + } + // Otherwise, just store this link keyed by its parent path; the expand_map + // is automatically derived from parent paths. + else { + $parent_path = $result[$link['plid']]['path']; + } + + $tree_dynamic[$parent_path][] = $link; + } + + return $tree_dynamic; +} + +/** + * Walk through the entire menu tree and merge in expanded dynamic menu links. + * + * @param &$tree + * A menu tree structure as returned by menu_tree_all_data(). + * @param $tree_dynamic + * A dynamic menu tree structure as returned by admin_menu_tree_dynamic(). + * @param $expand_map + * @todo Rename this argument, it's not the same $expand_map like elsewhere. + * Placeholder expansion arguments. + * @param $hidden + * An array containing links to hide, keyed by path. + * + * @see hook_admin_menu_map() + * @see menu_tree_all_data() + * @see admin_menu_tree_dynamic() + */ +function admin_menu_merge_tree(array &$tree, array $tree_dynamic, array $expand_map, array $hidden) { + foreach ($tree as $key => $data) { + $path = $data['link']['router_path']; + + // Recurse into regular menu tree. + if ($tree[$key]['below']) { + admin_menu_merge_tree($tree[$key]['below'], $tree_dynamic, $expand_map, $hidden); + } + // Hide link if requested, but only if there is no dynamic tree data for it. + elseif (isset($hidden[$path]) && !isset($tree_dynamic[$path])) { + $tree[$key]['link']['access'] = FALSE; + continue; + } + // Nothing to merge, if this parent path is not in our dynamic tree. + if (!isset($tree_dynamic[$path])) { + continue; + } + + foreach ($tree_dynamic[$path] as $link) { + // If the local task has custom placeholder expansion arguments set, + // merge them with parent arguments. + if (isset($link['expand_map'])) { + $expand_map = array_merge($expand_map, $link['expand_map']); + } + + // Set up path arguments map; depends on whether the item is dynamic + // (contains placeholders) or not. + if (strpos($link['path'], '%') === FALSE) { + echo "
"; var_dump("IN"); echo "\n";
+ // Build static item and subtree.
+ $map = explode('/', $link['path']);
+ $item = admin_menu_translate($link, $map);
+ admin_menu_merge_tree($item, $tree_dynamic, $expand_map, $hidden);
+ $tree[$key]['below'] += $item;
+ }
+ else {
+ // Drop dynamic items without expansion parameters.
+ if (empty($expand_map)) {
+ continue;
+ }
+
+ // Expand anonymous to named placeholders.
+ // @see _menu_load_objects()
+ $active = '';
+ $path = explode('/', $link['path']);
+ $load_functions = unserialize($link['load_functions']);
+ foreach ($load_functions as $index => $function) {
+ if ($function) {
+ if (is_array($function)) {
+ list($function,) = each($function);
+ }
+ // Add the loader function name minus "_load".
+ $placeholder = '%' . substr($function, 0, -5);
+ $path[$index] = $placeholder;
+ }
+ }
+ $path = implode('/', $path);
+
+ // Create new menu items using expansion arguments.
+ foreach ($expand_map as $arguments) {
+ // Create the cartesian product for all arguments and create new
+ // menu items for each generated combination thereof.
+ foreach (admin_menu_expand_args($arguments) as $replacements) {
+ $map = explode('/', strtr($path, $replacements));
+ $item = admin_menu_translate($link, $map);
+ // Build subtree using current replacement arguments.
+ // @todo Avoid rebuilding this for each item.
+ $current_map = array();
+ foreach ($replacements as $placeholder => $value) {
+ $current_map[$placeholder] = array($value);
+ }
+ admin_menu_merge_tree($item, $tree_dynamic, array($current_map), $hidden);
+ $tree[$key]['below'] += $item;
+ }
+ }
+ }
+ }
+ // Sort new subtree items.
+ ksort($tree[$key]['below']);
+ }
+}
+
+/**
+ * Create a menu item suitable for rendering.
+ *
+ * @param $router_item
+ * A menu router item.
+ * @param $map
+ * A path map with placeholders replaced.
+ */
+function admin_menu_translate($router_item, $map) {
+ _menu_translate($router_item, $map, TRUE);
+
+ // Run through hook_translated_menu_link_alter() to add devel information,
+ // if configured.
+ $router_item['menu_name'] = 'management';
+ admin_menu_translated_menu_link_alter($router_item, NULL);
+
+ if ($router_item['access']) {
+ // Turn local task menu callbacks into regular menu items, otherwise they
+ // won't be visible.
+ if ($router_item['type'] == MENU_CALLBACK) {
+ $router_item['type'] = MENU_NORMAL_ITEM;
+ }
+ // Fill in pseudo menu link values for rendering later.
+ $router_item['router_path'] = $router_item['path'];
+ $router_item['mlid'] = $router_item['href'];
+ // @todo Strip potential HTML from titles?
+ $router_item['title'] = strip_tags($router_item['title']);
+
+ $key = (50000 + $router_item['weight']) . ' ' . $router_item['title'] . ' ' . $router_item['mlid'];
+ return array($key => array(
+ 'link' => $router_item,
+ 'below' => array(),
+ ));
+ }
+
+ return array();
+}
+
+/**
+ * Create the cartesian product of multiple varying sized argument arrays.
+ *
+ * @param $arguments
+ * A two dimensional array of arguments.
+ *
+ * @see hook_admin_menu_map()
+ */
+function admin_menu_expand_args($arguments) {
+ $replacements = array();
+
+ // Initialize line cursors, move out array keys (placeholders) and assign
+ // numeric keys instead.
+ $i = 0;
+ $placeholders = array();
+ $new_arguments = array();
+ foreach ($arguments as $placeholder => $values) {
+ // Skip empty arguments.
+ if (empty($values)) {
+ continue;
+ }
+ $cursor[$i] = 0;
+ $placeholders[$i] = $placeholder;
+ $new_arguments[$i] = $values;
+ $i++;
+ }
+ $arguments = $new_arguments;
+ unset($new_arguments);
+
+ if ($rows = count($arguments)) {
+ do {
+ // Collect current argument from each row.
+ $row = array();
+ for ($i = 0; $i < $rows; ++$i) {
+ $row[$placeholders[$i]] = $arguments[$i][$cursor[$i]];
+ }
+ $replacements[] = $row;
+
+ // Increment cursor position.
+ $j = $rows - 1;
+ $cursor[$j]++;
+ while (!array_key_exists($cursor[$j], $arguments[$j])) {
+ // No more arguments left: reset cursor, go to next line and increment
+ // that cursor instead. Repeat until argument found or out of rows.
+ $cursor[$j] = 0;
+ if (--$j < 0) {
+ // We're done.
+ break 2;
+ }
+ $cursor[$j]++;
+ }
+ } while (1);
+ }
+
+ return $replacements;
+}
+
+/**
* Build the administration menu as renderable menu links.
*
* @param $tree
@@ -22,8 +343,8 @@
function admin_menu_links_menu($tree) {
$links = array();
foreach ($tree as $data) {
- // Skip menu callbacks (mostly dynamic items).
- if (isset($data['link']['type']) && $data['link']['type'] == MENU_CALLBACK) {
+ // Skip invisible items.
+ if (!$data['link']['access'] || $data['link']['type'] == MENU_CALLBACK) {
continue;
}
// Hide 'Administer' and make child links appear on this level.
diff --git admin_menu.install admin_menu.install
index 69c4d68..c0309ae 100644
--- admin_menu.install
+++ admin_menu.install
@@ -88,3 +88,16 @@ function admin_menu_update_7302() {
->execute();
}
+/**
+ * Remove local tasks from {menu_links} table.
+ */
+function admin_menu_update_7303() {
+ $paths = db_query('SELECT path FROM {menu_router} WHERE path LIKE :prefix AND type & :type', array(
+ ':prefix' => 'admin/%',
+ ':type' => MENU_IS_LOCAL_TASK,
+ ))->fetchCol();
+ db_delete('menu_links')
+ ->condition('router_path', $paths, 'IN')
+ ->execute();
+}
+
diff --git admin_menu.map.inc admin_menu.map.inc
new file mode 100644
index 0000000..07f2051
--- /dev/null
+++ admin_menu.map.inc
@@ -0,0 +1,123 @@
+ 'admin/config/content/formats',
+ 'hide' => 'admin/config/content/formats/list',
+ 'arguments' => array(
+ array('%filter_format' => array_keys(filter_formats())),
+ ),
+ );
+ return $map;
+}
+
+/**
+ * Implements hook_admin_menu_map() on behalf of Menu module.
+ */
+function menu_admin_menu_map() {
+ $map['admin/structure/menu/manage/%menu'] = array(
+ 'parent' => 'admin/structure/menu',
+ 'hide' => 'admin/structure/menu/list',
+ 'arguments' => array(
+ array('%menu' => array_keys(menu_get_menus())),
+ ),
+ );
+ return $map;
+}
+
+/**
+ * Implements hook_admin_menu_map() on behalf of Node module.
+ */
+function node_admin_menu_map() {
+ $map['admin/structure/types/manage/%node_type'] = array(
+ 'parent' => 'admin/structure/types',
+ 'hide' => 'admin/structure/types/list',
+ 'arguments' => array(
+ array('%node_type' => array_keys(node_type_get_types())),
+ ),
+ );
+ return $map;
+}
+
+/**
+ * Implements hook_admin_menu_map() on behalf of Field UI module.
+ */
+function field_ui_admin_menu_map() {
+ foreach (entity_get_info() as $obj_type => $info) {
+ foreach ($info['bundles'] as $bundle_name => $bundle_info) {
+ if (isset($bundle_info['admin'])) {
+ $arguments = array();
+ switch ($obj_type) {
+ case 'node':
+ // Provide replacement sets for content types, note that the
+ // fields (%field_ui_menu) are different for each content type.
+ foreach (array_keys($info['bundles']) as $node_type) {
+ $fields = array();
+ foreach (field_info_instances($obj_type, $node_type) as $field) {
+ $fields[] = $field['field_name'];
+ }
+ $arguments[] = array(
+ '%node_type' => array($node_type),
+ '%field_ui_menu' => $fields,
+ );
+ }
+ break;
+
+ case 'user':
+ $arguments = array(array('%field_ui_menu' => array_keys(field_info_fields('user'))));
+ break;
+ }
+ if (!empty($arguments)) {
+ $path = $bundle_info['admin']['path'];
+ $map["$path/fields/%field_ui_menu"] = array(
+ 'parent' => "$path/fields",
+ 'arguments' => $arguments,
+ );
+ }
+ }
+ }
+ }
+ return $map;
+}
+
+/**
+ * Implements hook_admin_menu_map() on behalf of Taxonomy module.
+ */
+function taxonomy_admin_menu_map() {
+ $map['admin/structure/taxonomy/%taxonomy_vocabulary'] = array(
+ 'parent' => 'admin/structure/taxonomy',
+ 'hide' => 'admin/structure/taxonomy/list',
+ 'arguments' => array(
+ array('%taxonomy_vocabulary' => array_keys(taxonomy_get_vocabularies())),
+ ),
+ );
+ return $map;
+}
+
+/**
+ * Implements hook_admin_menu_map() on behalf of Views UI module.
+ */
+function views_ui_admin_menu_map() {
+ // @todo Requires patch to views_ui.
+ $map['admin/structure/views/edit/%views_ui_cache'] = array(
+ 'parent' => 'admin/structure/views',
+ 'hide' => 'admin/structure/views/list',
+ 'arguments' => array(
+ array('%views_ui_cache' => array_keys(views_get_all_views())),
+ ),
+ );
+ return $map;
+}
+
diff --git admin_menu.module admin_menu.module
index b093f50..1fee7f0 100644
--- admin_menu.module
+++ admin_menu.module
@@ -406,7 +406,7 @@ function admin_menu_output() {
module_load_include('inc', 'admin_menu');
// Add administration menu.
- $content['menu'] = admin_menu_links_menu(menu_tree_all_data('management'));
+ $content['menu'] = admin_menu_links_menu(admin_menu_tree('management'));
$content['menu']['#theme'] = 'admin_menu_links';
// Ensure the menu tree is rendered between the icon and user links.
$content['menu']['#weight'] = 0;