Marc Ermshaus’ avatar

Marc Ermshaus

Linkblog

Algorithmic Advent: 16 – Expandable tree menu with PHP and jQuery

Published on 16 Dec 2010. Tagged with php, algorithmicadvent.

<?php
/**
 * This is an example on how to create an expandable tree menu structure with
 * PHP, CSS and jQuery.
 *
 * @version 2010-Dec-16
 *
 * @author Marc Ermshaus <http://www.ermshaus.org/>
 * @license GNU General Public License <http://www.gnu.org/licenses/gpl.html>
 */

define('X_BASEURL', '/nb/menutest');
define('X_CHARSET', 'UTF-8');

/**
 * Assembles an internal URL
 *
 * @param string $path
 * @param array $queryPart
 * @return string The assembled URL
 */
function url($path, $queryPart = array())
{
    $baseUrl = X_BASEURL;
    
    $url = $baseUrl . $path;

    if (count($queryPart) > 0) {
        $url .= '?' . http_build_query($queryPart);
    }

    return $url;
}

/**
 * Escapes a string for HTML display
 *
 * @param strng $s
 * @param int $quoteStyle
 * @param string $charset
 * @return string
 */
function escape($s, $quoteStyle = ENT_QUOTES, $charset = X_CHARSET)
{
    return htmlspecialchars($s, $quoteStyle, $charset);
}

/**
 * Arranges the input data into a tree structure
 *
 * @param array $data
 * @param int|null $parentId
 * @return array
 */
function toTree(array $data, $parentId = null)
{
    $rec = function (array $data, array &$root, $parent_id = null) use (&$rec)
    {
        $root['children'] = array();

        foreach ($data as $item) {
            if ($item['parent_id'] === $parent_id) {

                $newChild = array('data' => $item['data']);

                $root['children'][] = &$newChild;

                $rec($data, $newChild, $item['id']);
                unset($newChild);
            }
        }
    };

    $root = array('title' => 'root');

    $rec($data, $root, $parentId);

    return $root;
}

/**
 * Creates the HTML output for a navigation tree
 *
 * @param array $root Tree root (see toTree function)
 * @return string HTML code of navigation
 */
function menuHelper($root)
{
    $s = '';

    $s .= '<ul id="navigation">' . "\n";

    $recm = function (array $node, $depth = 0) use (&$recm)
    {
        $pad = '    ';

        foreach ($node['children'] as $child) {
            $spanClasses = 'title ';

            if (count($child['children']) > 0) {
                $spanClasses .= 'hasChildren ';
            }

            $spanClasses = trim($spanClasses);

            $s .= str_repeat($pad, $depth) . '<li>';
            
            $s .= '<span class="'.$spanClasses.'">';

            if ($child['data']['path'] !== null) {
                $s .= '<a href="'.url($child['data']['path']).'">'
                    . escape($child['data']['title']) . '</a>';
            } else {
                $s .= escape($child['data']['title']);
            }

            $s .= '</span>';

            if (count($child['children']) > 0) {
                $s .= "\n";

                $depth++;

                $s .= str_repeat($pad, $depth) . '<ul>' . "\n";
                $s .= $recm($child, $depth + 1);
                $s .= str_repeat($pad, $depth) . '</ul>' . "\n";

                $depth--;
                
                $s .= str_repeat($pad, $depth);
            }

            $s .= '</li>' . "\n";
        }

        return $s;
    };

    $s .= $recm($root, 1);

    $s .= '</ul>' . "\n";

    return $s;
}

$menuData = array(
    array('id'        => 1,
          'parent_id' => null,
          'data'      => array('title' => 'Item 1',
                               'path'  => '/item1')),
    array('id'        => 2,
          'parent_id' => null,
          'data'      => array('title' => 'Item 2',
                               'path'  => '/item2')),
    array('id'        => 3,
          'parent_id' => 1,
          'data'      => array('title' => 'Item 1.1',
                               'path'  => '/item1/item1')),
    array('id'        => 4,
          'parent_id' => 2,
          'data'      => array('title' => 'Item 2.1',
                               'path'  => '/item2/item1')),
    array('id'        => 5,
          'parent_id' => 2,
          'data'      => array('title' => 'Item 2.2',
                               'path'  => '/item2/item2')),
    array('id'        => 6,
          'parent_id' => 1,
          'data'      => array('title' => 'Item 1.2',
                               'path'  => '/item1/item2')),
    array('id'        => 7,
          'parent_id' => 4,
          'data'      => array('title' => 'Item 2.1.1',
                               'path'  => '/item2/item1/item1')),
    array('id'        => 8,
          'parent_id' => null,
          'data'      => array('title' => 'Item 3',
                               'path'  => '/item3'))
);

?><!DOCTYPE html>

<html>

    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Expandable tree menu</title>
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>

        <style type="text/css">
        #navigation .title {
            cursor: pointer;
        }

        #navigation .hasChildren {
            background: #9ff;
        }

        #navigation .hasChildren:after {
            content: " (expand)";
        }

        #navigation .hidden {
            display: none;
        }

        #navigation .open-category {
            
        }

        /* "#navigation .open-category > .title" or
           "#navigation .title.open" won't work in IE6 */
        #navigation .open-title {
            color: #f00;
        }
        </style>

        <script type="text/javascript">
        $(document).ready(function () {
            $('#navigation ul').addClass('hidden');
            $('#navigation li').click(function (event) {
                // Entry has children?
                if ($(this).children('ul').length > 0) {
                    $(this).toggleClass('open-category');
                    $(this).children('.title').toggleClass('open-title');
                    $(this).children('ul').toggleClass('hidden');
                }
                event.stopPropagation();
            });
        });
        </script>
    </head>

    <body>

        <?php echo menuHelper(toTree($menuData)); ?>

    </body>

</html>