eo

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

Written on 16 Dec 2010. Tags: post , dc:language=en , 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>

298/316