WordPress API Menu/Submenu Order

I’m developing a child-theme using WordPress 3.4.2 and the development version of the Options Framework by David Price. This is my first theme and I’m relatively new to this, so I’ve had a look into the WordPress Codex and checked out registering items into the API.

Without tampering with any external files outside of my theme, I was wondering if there was a way to re-arrange where the Theme Options page is located within the hierarchy of the Appearance menu – so when my theme is activated, the position is not like the first image but like the second.

Read More

oldnew

I know you can create either a menu (such as the Appearance tab, Plugins, Users etc.) or a sub-menu (Themes, Widgets, Menus etc.), but how would I go about setting a sub-menu say, second from the top?

From what I gather, somewhere there is an order being called and any other additional pages within the functions.php file are placed after those?

In my functions.php file:

// Add our "Theme Options" page to the WordPress API admin menu.
if ( !function_exists( 'optionsframework_init' ) ) {
    define( 'OPTIONS_FRAMEWORK_DIRECTORY', get_template_directory_uri() . '/inc/' );
    require_once dirname( __FILE__ ) . '/inc/options-framework.php';
}

Thanks.

Related posts

Leave a Reply

3 comments

  1. Here’s an example;

    First to figure out the order of the sub menu items based upon its array key you can do a var_dump on the $submenu global variable which will output the following;

    (I’m using the Posts menu and sub menu as an example)

      //shortened for brevity....
    
      ["edit.php"]=>
      array(6) {
        [5]=>
        array(3) {
          [0]=> string(9) "All Posts"
          [1]=> string(10) "edit_posts"
          [2]=> string(8) "edit.php"
        }
        [10]=>
        array(3) {
          [0]=> string(7) "Add New"
          [1]=> string(10) "edit_posts"
          [2]=> string(12) "post-new.php"
        }
        [15]=>
        array(3) {
          [0]=> string(10) "Categories"
          [1]=> string(17) "manage_categories"
          [2]=> string(31) "edit-tags.php?taxonomy=category"
        }
        [17]=>
        array(3) {
          [0]=> string(14) "Sub Menu Title"
          [1]=> string(10) "edit_posts"
          [2]=> string(17) "sub_menu_page.php"
        }
      }
    

    We can see that my sub menu item gets added into the array with a key of 17 after the default items.

    If for example I want to add my sub menu item, directly after the All Posts sub menu item I need to do so by setting my array key to either 6, 7, 8 or 9 (anything after 5 and before 10 respectively.

    This is how you do it…

    function change_submenu_order() {
    
        global $menu;
        global $submenu;
    
         //set our new key
        $new_key['edit.php'][6] = $submenu['edit.php'][17];
    
        //unset the old key
        unset($submenu['edit.php'][17]);
    
        //get our new key back into the array
        $submenu['edit.php'][6] = $new_key['edit.php'][6];
    
    
        //sort the array - important! If you don't the key will be appended
        //to the end of $submenu['edit.php'] array. We don't want that, we
        //our keys to be in descending order
        ksort($submenu['edit.php']);
    
    }
    

    Result,

      ["edit.php"]=>
      array(6) {
        [5]=>
        array(3) {
          [0]=> string(9) "All Posts"
          [1]=> string(10) "edit_posts"
          [2]=> string(8) "edit.php"
        }
        [6]=>
        array(3) {
          [0]=> string(14) "Sub Menu Title"
          [1]=> string(10) "edit_posts"
          [2]=> string(17) "sub_menu_page.php"
        }
        [10]=>
        array(3) {
          [0]=> string(7) "Add New"
          [1]=> string(10) "edit_posts"
          [2]=> string(12) "post-new.php"
        }
        [15]=>
        array(3) {
          [0]=> string(10) "Categories"
          [1]=> string(17) "manage_categories"
          [2]=> string(31) "edit-tags.php?taxonomy=category"
        }
      }
    

    …give it a try and let us know how you go!

    Update 1:

    Add this to your functions.php file;

    function change_post_menu_label() {
    
        global $menu;
        global $submenu;
    
        $my_menu  = 'example_page'; //set submenu page via its ID
        $location = 1; //set the position (1 = first item etc)
        $target_menu = 'edit.php'; //the menu we are adding our item to
    
        /* ----- do not edit below this line ----- */
    
    
        //check if our desired location is already used by another submenu item
        //if TRUE add 1 to our value so menu items don't clash and override each other
        $existing_key = array_keys( $submenu[$target_menu] );
        if ($existing_key = $location)
        $location = $location + 1;
    
        $key = false;
        foreach ( $submenu[$target_menu] as $index => $values ){
    
            $key = array_search( $my_menu, $values );
    
            if ( false !== $key ){
                $key = $index;
                break;
            }
        }
    
         $new['edit.php'][$location] = $submenu[$target_menu][$key];
         unset($submenu[$target_menu][$key]);
         $submenu[$target_menu][$location] = $new[$target_menu][$location];
    
        ksort($submenu[$target_menu]);
    
    }
    

    My update includes a slightly easier way to handle the setting of your menu position, you need only stipulate the name of your submenu page and the position you want within the menu. However if you select a submenu page $location equal to that of an existing key, it will override that key with yours, thus the menu item will disappear with your menu item in its place. Increment or decrement the number to correctly order your menu if that is the case. Similar, if someone installs a plugin that effects that same menu area, and for which has the same $location as your submenu item then the same problem will occur. To circumvent that, Kaiser’s example provides some basic checking for that.

    Update 2:

    I’ve added an additional block of code that checks all existing keys in the array against our desired $location and if a match is found we will increment our $location value by 1 in order to avoid menu items overriding each other. This is the code responsible for that,

       //excerpted snippet only for example purposes (found in original code above)
       $existing_key = array_keys( $submenu[$target_menu] );
       if ($existing_key = $location)
       $location = $location + 1;
    

    Update 3: (script revised to allow sorting of multiple sub menu items)

    add_action('admin_init', 'move_theme_options_label', 999);
    
    function move_theme_options_label() {
        global $menu;
        global $submenu;
    
    $target_menu = array(
        'themes.php' => array(
            array('id' => 'optionsframework', 'pos' => 2),
            array('id' => 'bp-tpack-options', 'pos' => 4),
            array('id' => 'multiple_sidebars', 'pos' => 3),
            )
    );
    
    $key = false;
    
    foreach ( $target_menu as $menus => $atts ){
    
        foreach ($atts as $att){
    
            foreach ($submenu[$menus] as $index => $value){
    
            $current = $index;  
    
            if(array_search( $att['id'], $value)){ 
            $key = $current;
            }
    
                while (array_key_exists($att['pos'], $submenu[$menus]))
                    $att['pos'] = $att['pos'] + 1;
    
                if ( false !== $key ){
    
                    if (array_key_exists($key, $submenu[$menus])){
                        $new[$menus][$key] = $submenu[$menus][$key];
                        unset($submenu[$menus][$key]);
                        $submenu[$menus][$att['pos']] = $new[$menus][$key];
    
                    } 
                }
            }
        }
    }
    
    ksort($submenu[$menus]);
    return $submenu;
    
    }
    

    In the example above you can target multiple sub menus and multiple items per sub menu by setting the parameters accordingly within the $target_menu variable which holds a multi-dimensional array of values.

    $target_menu = array(
    //menu to target (e.g. appearance menu)
    'themes.php' => array(
        //id of menu item you want to target followed by the position you want in sub menu
        array('id' => 'optionsframework', 'pos' => 2),
        //id of menu item you want to target followed by the position you want in sub menu
        array('id' => 'bp-tpack-options', 'pos' => 3),
        //id of menu item you want to target followed by the position you want in sub menu
        array('id' => 'multiple_sidebars', 'pos' => 4),
        )
     //etc....
    );
    

    This revision will prevent sub menu items over-writing each other if they have the same key (position), as it will cycle through until it finds an available key (position) that does not exist.

  2. The admin menu (and its problems)

    As the admin menu seriously lacks any hooks and a public API (that allow moving the items around), you have to use some workarounds. The following answer shows you what is awaiting you in the future and how you can work around as long as we have the current state of core.

    First I have to note, that scribu is working on an admin menu patch that should make the handling much easier. The current structure is pretty messed up and I have written an article about it that will soon be outdated. Expect WP 3.6 to change things completely.

    Then there’s also the point, that you shouldn’t use Options pages for themes anymore. There’s – nowadays – the »Theme Customizer« for that.

    The plugin

    I wrote a plugin that tests this with the default “Theme Options” page for the TwentyEleven/Ten options page. As you can see, there’s no real API that allows any position. So we have to intercept the global.

    In short: Just follow the comments and take a look at the admin notices, that I added to give you some debug output.

    <?php
    /** Plugin Name: (#70916) Move Submenu item */
    
    add_action( 'plugins_loaded', array( 'wpse70916_admin_submenu_items', 'init' ) );
    
    class wpse70916_admin_submenu_items
    {
        protected static $instance;
    
        public $msg;
    
        public static function init()
        {
            is_null( self :: $instance ) AND self :: $instance = new self;
            return self :: $instance;
        }
    
        public function __construct()
        {
            add_action( 'admin_notices', array( $this, 'add_msg' ) );
    
            add_filter( 'parent_file', array( $this, 'move_submenu_items' ) );
        }
    
        public function move_submenu_items( $parent_file )
        {
            global $submenu;
            $parent = $submenu['themes.php'];
    
            $search_for = 'theme_options';
    
            // Find current position
            $found = false;
            foreach ( $parent as $pos => $item )
            {
                $found = array_search( $search_for, $item );
                if ( false !== $found )
                {
                    $found = $pos;
                    break;
                }
            }
            // DEBUG: Tell if we didn't find it.
            if ( empty( $found ) )
                return $this->msg = 'That search did not work out...';
    
            // Now we need to determine the first and second item position
            $temp = array_keys( $parent );
            $first_item  = array_shift( $temp );
            $second_item = array_shift( $temp );
    
            // DEBUG: Check if it the item fits between the first two items:
            $distance = ( $second_item - $first_item );
            if ( 1 >= $distance )
                return $this->msg = 'We do not have enough space for your item';
    
            // Temporary container for our item data
            $target_data = $parent[ $found ];
    
            // Now we can savely remove the current options page
            if ( false === remove_submenu_page( 'themes.php', $search_for ) )
                return $this->msg = 'Failed to remove the item';
    
            // Shuffle items (insert options page)
            $submenu['themes.php'][ $first_item + 1 ] = $target_data;
            // Need to resort the items by their index/key
            ksort( $submenu['themes.php'] );
        }
    
        // DEBUG Messages
        public function add_msg()
        {
            return print sprintf(
                 '<div class="update-nag">%s</div>'
                ,$this->msg
            );
        }
    } // END Class wpse70916_admin_submenu_items
    

    Good luck and have fun.

  3. Custom filters

    There is be another possibility to achieve this. Don’t ask me why I haven’t thought earlier about it. Anyway, there’s a filter dedicated to a custom menu order. Simply set it to true to allow a custom order. Then you got a second hook to order the main menu items. In there we just intercept the global $submenu and switch around our sub menu items.

    enter image description here

    This example moves the Menus item above the Widgets item to demonstrate its functionality. You can adjust it to what you like.

    <?php
    defined( 'ABSPATH' ) OR exit;
    /**
     * Plugin Name: (#70916) Custom Menu Order
     * Description: Changes the menu order of a submenu item.
     */
    
    // Allow a custom order
    add_filter( 'custom_menu_order', '__return_true' );
    add_filter( 'menu_order', 'wpse70916_custom_submenu_order' );
    function wpse70916_custom_submenu_order( $menu )
    {
        // Get the original position/index
        $old_index = 10;
        // Define a new position/index
        $new_index = 6;
    
        // We directly interact with the global
        $submenu = &$GLOBALS['submenu'];
        // Assign our item at the new position/index
        $submenu['themes.php'][ $new_index ] = $submenu['themes.php'][ $old_index ];
        // Get rid of the old item
        unset( $submenu['themes.php'][ $old_index ] );
        // Restore the order
        ksort( $submenu['themes.php'] );
    
        return $menu;
    }