Help extending custom drag-drop page ordering on admin page list screen

I am utilizing the following code which transforms any hierarchical page or custom post type with the ability to reorder the the pages through simple drag/drop. (also this code also adds a new display filter to hierarchical pages to make things simpler).

The problem I need help with which currently does not work is being able to drag one page tree into another page tree or out of an existing page tree.

Could someone please provide the code modifications required to extend this capability?

If you know of a better way to achieve the same objectives with different code instead of extending the code provided below please also post it.

UPDATED: I have also posted the code here: https://gist.github.com/812204

NOTE: for all those individuals who would like to test this out just do the follow.

1st Copy/past the code below in your themes functions.php” file.

///////////////////////////////////////////////////////////////////////////////////////////
// CODE TO ADD POST PER PAGE FILTER
///////////////////////////////////////////////////////////////////////////////////////////    
    add_filter( 'edit_posts_per_page', 'reorder_edit_posts_per_page', 10, 2 );
    function reorder_edit_posts_per_page( $per_page, $post_type ) {

        // CHECK USER PERMISSIONS
        if ( !current_user_can('edit_others_pages') )
            return;
        $post_type_object = get_post_type_object( $post_type );

        // ONLY APPLY TO HIERARCHICAL POST TYPE
        if ( !$post_type_object->hierarchical )
            return;

        // ADD POST PER PAGE DROP DOWN UI
        add_action( 'restrict_manage_posts', 'reorder_posts_per_page_filter' );

        // ADD SPECIAL STYLES (MOVE CURSOR & SPINNING LOADER AFTER REORDER)
        wp_enqueue_script( 'page-ordering', get_bloginfo('stylesheet_directory') . '/custom/js/page-resorting.js', array('jquery-ui-sortable'), '0.8.4', true );
        add_action( 'admin_print_styles', 'reorder_admin_styles' );

        if ( isset( $_GET['spo'] ) && is_numeric( $_GET['spo'] ) && ( $_GET['spo'] == -1 || ($_GET['spo']%10) == 0 ) ) :
            global $edit_per_page, $user_ID;
            $per_page = $_GET['spo'];
            if ( $per_page == -1 )
                $per_page = 99999;
            update_user_option( $user_ID, $edit_per_page, $per_page );
        endif;
        return $per_page;
    }


    // STYLING CSS FOR THE AJAX
       function reorder_admin_styles() {
        echo '<style type="text/css">table.widefat tbody th, table.widefat tbody td { cursor: move; }</style>';
       }


    // FUNCTION TO CREATE THE NUMBER OF POSTS PER PAGE DROPDOWN UI
       function reorder_posts_per_page_filter() {
        global $per_page;       
        $spo = isset($_GET['spo']) ? (int)$_GET['spo'] : $per_page;
       ?>
        Display:<select name="spo" style="width: 100px;">
            <option<?php selected( $spo, -1 ); ?> value="-1"><?php _e('All Results'); ?></option>
            <?php for( $i=10;$i<=100;$i+=10 ) : ?>
            <option<?php selected( $spo, $i ); ?> value="<?php echo $i ?>"><?php echo $i; ?> <?php _e('Results'); ?></option>
            <?php endfor; ?>        
        </select>
       <?php
       } 



    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // ACTUAL AJAX REQUEST FOR SORTING PAGES 
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

    add_action( 'wp_ajax_simple_page_ordering', 'reorder_do_page_ordering' );
    function reorder_do_page_ordering() {

        // RECHECK PERMISSIONS
        if ( !current_user_can('edit_others_pages') || !isset($_POST['id']) || empty($_POST['id']) || ( !isset($_POST['previd']) && !isset($_POST['nextid']) ) )
            die(-1);

        // IS IT A REAL POST?
        if ( !$post = get_post( $_POST['id'] ) )
            die(-1);
        $previd = isset($_POST['previd']) ? $_POST['previd'] : false;
        $nextid = isset($_POST['nextid']) ? $_POST['nextid'] : false;
        if ( $previd ) {

            // FETCH ALL THE SIBLINGS (RELATIVE ORDERING)
            $siblings = get_posts(array( 'depth' => 1, 'numberposts' => -1, 'post_type' => $post->post_type, 'post_status' => 'publish,pending,draft,future,private', 'post_parent' => $post->post_parent, 'orderby' => 'menu_order', 'order' => 'ASC', 'exclude' => $post->ID ));
            foreach( $siblings as $sibling ) :

                // BEGIN UPDATING MENU ORDERS
                if ( $sibling->ID == $previd ) {
                    $menu_order = $sibling->menu_order + 1;
                    // UPDATE THE ACTUAL MOVED POST TO 1 AFTER PREV
                    wp_update_post(array( 'ID' => $post->ID, 'menu_order' => $menu_order ));
                    continue;
                }

                // NOTHING LEFT TO DO - NUMBERS CORRECTLY PADDED
                if ( isset($menu_order) && $menu_order < $sibling->menu_order )
                    break; 

                // NEED TO UPDATE THE SIBLINGS MENU ORDER AS WELL
                if ( isset($menu_order) ) {
                    $menu_order++;
                    // UPDATE THE ACTUAL MOVED POST TO 1 AFTER PREV
                    wp_update_post(array( 'ID' => $sibling->ID, 'menu_order' => $menu_order )); 
                }       
            endforeach;
        }

        if ( !isset($menu_order) && $nextid ) {

            // FETCH ALL THE SIBLINGS (RELATIVE ORDERING)
            $siblings = get_posts(array( 'depth' => 1, 'numberposts' => -1, 'post_type' => $post->post_type, 'post_status' => 'publish,pending,draft,future,private', 'post_parent' => $post->post_parent, 'orderby' => 'menu_order', 'order' => 'DESC', 'exclude' => $post->ID ));
            foreach( $siblings as $sibling ) :

                // START UPDATING MENU ORDERS
                if ( $sibling->ID == $nextid ) {
                    $menu_order = $sibling->menu_order - 1;
                    // UPDATE THE ACTUAL MOVED POST TO 1 AFTER PREV
                    wp_update_post(array( 'ID' => $post->ID, 'menu_order' => $menu_order ));
                    continue;
                }

                // NOTHING LEFT TO DO - NUMBER ALREADY PADDED
                if ( isset($menu_order) && $menu_order > $sibling->menu_order )
                    break; 

                // NEED TO UPDATE THE SIBLING MENU ORDER
                if ( isset($menu_order) ) {
                    $menu_order--;
                    // UPDATE THE ACTUAL MOVED POST TO 1 AFTER PREV
                    wp_update_post(array( 'ID' => $sibling->ID, 'menu_order' => $menu_order )); 
                }       
            endforeach;
        }

        // FETCH ALL THE SIBLINGS WITH RELATIVE ORDERING AND IF THE MOVED POST HAS CHILDREN REFRESH THE PAGE
        $children = get_posts(array( 'depth' => 1, 'numberposts' => 1, 'post_type' => $post->post_type, 'post_status' => 'publish,pending,draft,future,private', 'post_parent' => $post->ID ));
        if ( !empty($children) )
            die('children');
        die();
    }

2nd Steps
Because we the script below calls a custom js file we need to crate and include this file as well. If you don’t want to modify the script above then go and create a folder in the root directory of your theme folder and call it “custom”. Next, within this folder create another folder called “js”. Next create a new file within this folder called “page-resorting.js” and past the following code into that file. After this is done you should be able to reorder through drag/drop all pages in the admin area.

/////////////////////////////////////////////////////////////////////////////////////////////////////
// THIS SCRIPT APPLIES TO THE CUSTOM SCRIPT MODIFICATION ALLOWING HIERARCHICAL PAGES TO BE REORDERED
/////////////////////////////////////////////////////////////////////////////////////////////////////


jQuery("table.widefat tbody").sortable({  
    cursor: 'move',
    axis: 'y',
    containment: 'table.widefat',
    scrollSensitivity: 40,
    helper: function(e, ui) {                   
        ui.children().each(function() { jQuery(this).width(jQuery(this).width()); });
        return ui;
    },
    start: function(event, ui) {
        if ( ! ui.item.hasClass('alternate') ) ui.item.css( 'background-color', '#ffffff' );
        ui.item.children('td, th').css('border','none');
        ui.item.css( 'outline', '1px solid #dfdfdf' );
    },
    stop: function(event, ui) {     
        ui.item.removeAttr('style');
        ui.item.children('td, th').removeAttr('style');
    },
    update: function(event, ui) {   
        if ( ui.item.hasClass('inline-editor') ) {
            jQuery("table.widefat tbody").sortable('cancel');
            alert( 'Please close the quick editor before reordering this item.' );
            return;
        }

        var postid = ui.item.find('.check-column input').val(); // THIS POST ID
        var postparent = ui.item.find('.post_parent').html();   // POST PARENT

        var prevpostid = ui.item.prev().find('.check-column input').val();
        var nextpostid = ui.item.next().find('.check-column input').val();

        // can only sort in same tree

        var prevpostparent = undefined;
        if ( prevpostid != undefined ) {
            var prevpostparent = ui.item.prev().find('.post_parent').html()
            if ( prevpostparent != postparent) prevpostid = undefined;
        }

        var nextpostparent = undefined;
        if ( nextpostid != undefined ) {
            nextpostparent = ui.item.next().find('.post_parent').html();
            if ( nextpostparent != postparent) nextpostid = undefined;
        }   

        // DISPLAY AN ALERT MESSAGE IF ANY OF THE FOLLOWING TAKES PLACE
        // IF PREVIOUS AND NEXT ARE NOT AT THE SAME TREE LEVEL OR NOT AT THE SAME TREE LEVEL AND THE PREVIOUS PAGE IS THE PARENT OF THE NEXT OR JUST MOVED BENEATH ITS OWN CHILDREN                     
        if ( ( prevpostid == undefined && nextpostid == undefined ) || ( nextpostid == undefined && nextpostparent == prevpostid ) || ( nextpostid != undefined && prevpostparent == postid ) ) {
            jQuery("table.widefat tbody").sortable('cancel');
            alert( "SORRY, THIS ACTION IS NOT POSSIBLE!\n\n>>> WHY THIS DOES NOT WORK:\nDrag-and-Drop capabilities only work for items within their current tree.\n\n>>> HERE IS HOW YOU CAN MOVE IT:\nIn order to move this item to the location you specified you simply need to use the \"Quick Edit\" link and modify the associated \"Parent\" page.\n\n>>> LOCATING THE QUICK EDIT LINK:\nOn the post you want to move, just hover over the post title and click on the \"Quick Edit\" link which appears below the title." );
            return;
        }

        // SHOW AJAX SPINNING SAVE ELEMENT
        ui.item.find('.check-column input').hide().after('<img alt="processing" src="https://wordpress.stackexchange.com/questions/2771/images/wpspin_light.gif" class="waiting" style="margin-left: 6px;" />');

        // EXECUTE THE SORTING VIA AJAX
        jQuery.post( ajaxurl, { action: 'simple_page_ordering', id: postid, previd: prevpostid, nextid: nextpostid }, function(response){           
            if ( response == 'children' ) window.location.reload();
            else ui.item.find('.check-column input').show().siblings('img').remove();
        });

        // FIX CELL COLORS
        jQuery( 'table.widefat tbody tr' ).each(function(){
            var i = jQuery('table.widefat tbody tr').index(this);
            if ( i%2 == 0 ) jQuery(this).addClass('alternate');
            else jQuery(this).removeClass('alternate');
        });
    }
}).disableSelection();

1 Answer
1

Not really an answer, but perhaps the answer lies within this somewhere. I unpacked the navmenu js script, and stripped out what appeared to be the code which makes the drag/drop sortable nested nav items possible. It’s not pretty.

I don’t think jQuery UI’s dragable/sortable modules support nesting elements, and that’s most likely the thing that will make or break whether you make it work that way. The navmenu script has a rather indepth and hard to decipher set of objects and functions that are used to calculate the inner and outer width, or depth’s. I would try to figure it out, but I have my own issue with my ajax post attachments uploading dynamic metabox plugin and making that work correctly still.

Maybe looking at this though will give you some insight to what you need to do in your code, if you are even able to using jqUI.

Here’s the entire nav-menu.js file unpacked, https://gist.github.com/820633
There are pieces you may need to see which tie this stuff together in order to make sense of it, which I didn’t include.

depthToPx: function (c) {
    return c * a.options.menuItemDepthPerLevel
},
pxToDepth: function (c) {
    return Math.floor(c / a.options.menuItemDepthPerLevel)
}

menuItemDepth: function () {
    var c = a.isRTL ? this.eq(0).css("margin-right") : this.eq(0).css("margin-left");
    return a.pxToDepth(c && -1 != c.indexOf("px") ? c.slice(0, -2) : 0)
},
updateDepthClass: function (d, c) {
    return this.each(function () {
        var e = b(this);
        c = c || e.menuItemDepth();
        b(this).removeClass("menu-item-depth-" + c).addClass("menu-item-depth-" + d)
    })
},
shiftDepthClass: function (c) {
    return this.each(function () {
        var d = b(this),
            e = d.menuItemDepth();
        b(this).removeClass("menu-item-depth-" + e).addClass("menu-item-depth-" + (e + c))
    })
},
childMenuItems: function () {
    var c = b();
    this.each(function () {
        var d = b(this),
            f = d.menuItemDepth(),
            e = d.next();
        while (e.length && e.menuItemDepth() > f) {
            c = c.add(e);
            e = e.next()
        }
    });
    return c
},
updateParentMenuItemDBId: function () {
    return this.each(function () {
        var e = b(this),
            c = e.find(".menu-item-data-parent-id"),
            f = e.menuItemDepth(),
            d = e.prev();
        if (f == 0) {
            c.val(0)
        } else {
            while (!d[0] || !d[0].className || -1 == d[0].className.indexOf("menu-item") || (d.menuItemDepth() != f - 1)) {
                d = d.prev()
            }
            c.val(d.find(".menu-item-data-db-id").val())
        }
    })
}

This is the sortables init method, which does some crazy algebraic E=Mc2 looking stuff lol

initSortables: function () {
    var p = 0,
        e, t, d, l, o, f, c, i, s, m = a.menuList.offset().left,
        h = b("body"),
        q, n = r();
    m += a.isRTL ? a.menuList.width() : 0;
    a.menuList.sortable({
        handle: ".menu-item-handle",
        placeholder: "sortable-placeholder",
        start: function (A, z) {
            var u, x, w, v, y;
            if (a.isRTL) {
                z.item[0].style.right = "auto"
            }
            s = z.item.children(".menu-item-transport");
            e = z.item.menuItemDepth();
            j(z, e);
            w = (z.item.next()[0] == z.placeholder[0]) ? z.item.next() : z.item;
            v = w.childMenuItems();
            s.append(v);
            u = s.outerHeight();
            u += (u > 0) ? (z.placeholder.css("margin-top").slice(0, -2) * 1) : 0;
            u += z.helper.outerHeight();
            i = u;
            u -= 2;
            z.placeholder.height(u);
            q = e;
            v.each(function () {
                var B = b(this).menuItemDepth();
                q = (B > q) ? B : q
            });
            x = z.helper.find(".menu-item-handle").outerWidth();
            x += a.depthToPx(q - e);
            x -= 2;
            z.placeholder.width(x);
            y = z.placeholder.next();
            y.css("margin-top", i + "px");
            z.placeholder.detach();
            b(this).sortable("refresh");
            z.item.after(z.placeholder);
            y.css("margin-top", 0);
            k(z)
        },
        stop: function (x, w) {
            var v, u = p - e;
            v = s.children().insertAfter(w.item);
            if (u != 0) {
                w.item.updateDepthClass(p);
                v.shiftDepthClass(u);
                g(u)
            }
            a.registerChange();
            w.item.updateParentMenuItemDBId();
            w.item[0].style.top = 0;
            if (a.isRTL) {
                w.item[0].style.left = "auto";
                w.item[0].style.right = 0
            }
            a.refreshMenuTabs(true)
        },
        change: function (v, u) {
            if (!u.placeholder.parent().hasClass("menu")) {
                (l.length) ? l.after(u.placeholder) : a.menuList.prepend(u.placeholder)
            }
            k(u)
        },
        sort: function (w, v) {
            var y = v.helper.offset(),
                u = a.isRTL ? y.left + v.helper.width() : y.left,
                x = a.negateIfRTL * a.pxToDepth(u - m);
            if (x > d || y.top < f) {
                x = d
            } else {
                if (x < t) {
                    x = t
                }
            }
            if (x != p) {
                j(v, x)
            }
            if (c && y.top + i > c) {
                o.after(v.placeholder);
                k(v);
                b(this).sortable("refreshPositions")
            }
        }
    });

    function k(u) {
        var v;
        l = u.placeholder.prev();
        o = u.placeholder.next();
        if (l[0] == u.item[0]) {
            l = l.prev()
        }
        if (o[0] == u.item[0]) {
            o = o.next()
        }
        f = (l.length) ? l.offset().top + l.height() : 0;
        c = (o.length) ? o.offset().top + o.height() / 3 : 0;
        t = (o.length) ? o.menuItemDepth() : 0;
        if (l.length) {
            d = ((v = l.menuItemDepth() + 1) > a.options.globalMaxDepth) ? a.options.globalMaxDepth : v
        } else {
            d = 0
        }
    }
    function j(u, v) {
        u.placeholder.updateDepthClass(v, p);
        p = v
    }
    function r() {
        if (!h[0].className) {
            return 0
        }
        var u = h[0].className.match(/menu-max-depth-(\d+)/);
        return u && u[1] ? parseInt(u[1]) : 0
    }
    function g(u) {
        var v, w = n;
        if (u === 0) {
            return
        } else {
            if (u > 0) {
                v = q + u;
                if (v > n) {
                    w = v
                }
            } else {
                if (u < 0 && q == n) {
                    while (!b(".menu-item-depth-" + w, a.menuList).length && w > 0) {
                        w--
                    }
                }
            }
        }
        h.removeClass("menu-max-depth-" + n).addClass("menu-max-depth-" + w);
        n = w
    }
}

Would be nice if they had a dev version of this file, like they do with other scripts in WP. The minified single letter vars are too much to handle for me right now.
It’s just Alphabet soup.

Leave a Comment