Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set CSS class on second level UL with Zend framework and bootstrap

The following code will set the class nav on the first level UL

$mainNav = public_nav_main();
$mainNav->setUlClass('nav')->setUlId('main-menu-left');

However im using bootstrap and so want the second level ul to have the class 'dropdown-menu'

I cant seem to find a reference to get this sorted.

Zend is being used as the base structure in the software im using, Omeka. Unfortunately Omeka doesnt have a way to do this natively so I am having to dive into the underlying Zend FW although I dont want to modify that too much as it might be changed.

like image 472
Paul M Avatar asked Jun 24 '15 09:06

Paul M


2 Answers

You might just want to write a totally new View Helper based off Zend_View_Helper_Navigation_HelperAbstract.

Looking on GitHub for a bootstrap compatible helper based on that abstract I did encounter this: https://github.com/michaelmoussa/zf1-navigation-view-helper-bootstrap/blob/master/library/ZFBootstrap/View/Helper/Navigation/Menu.php which takes an interesting approach, post processing the markup generated from the out-of-the-box helpers.

I took a slightly different approach recently and just hacked the heck out of Zend_View_Helper_Navigation_Menu. Here is a unified diff summarizing those changes: http://pastebin.com/mrJG8QCt Better though to extend the class...

I didn't deal with sub-menus, however the issues I ran into were...

  1. Way to add aria-role to <li> elements.
  2. Avoid collision when rendering the menu twice - two representations - collapsed bootstrap style and traditional one for larger viewports. -Maybe ZF already offered something to work around this? Didn't jump out at me if there was.

This code shows the methods you need to tweak:

class MyMenu extends Zend_View_Helper_Navigation_Menu
{

/**
 * Want a way to set aria role on menu li elements because its 2015 yo
 *
 * @var string
 */
protected $_liRole = '';

/**
 * Workaround so I can render the damn thing twice on the same page and not collide IDs on the <a>'s
 * Issue arose when adopting bootstrap and rendering both full page nav and collapsed nav bar
 *
 * @var string
 */
protected $_idAlias = '';

public function setLiRole($liRole)
{
    if (is_string($liRole)) {
        $this->_liRole = $liRole;
    }
    return $this;
}
public function getLiRole()
{
    return $this->_liRole;
}

public function setIdAlias($alias)
{
    $this->_idAlias = $alias;
    return $this;
}
public function getIdAlias()
{
    return $this->_idAlias;
}

public function renderMenu(Zend_Navigation_Container $container = null, array $options = array())   
{
    $this->setLiRole($options['liRole']);
    $this->setIdAlias($options['idAlias']);

    return parent::renderMenu($container, $options);
}

/**
 * Returns an HTML string containing an 'a' element for the given page if
 * the page's href is not empty, and a 'span' element if it is empty
 *
 * Overrides {@link Zend_View_Helper_Navigation_Abstract::htmlify()}.
 *
 * @param  Zend_Navigation_Page $page  page to generate HTML for
 * @return string                      HTML string for the given page
 */
public function htmlify(Zend_Navigation_Page $page)
{
    // get label and title for translating
    $label = $page->getLabel();
    $title = $page->getTitle();

    // translate label and title?
    if ($this->getUseTranslator() && $t = $this->getTranslator()) {
        if (is_string($label) && !empty($label)) {
            $label = $t->translate($label);
        }
        if (is_string($title) && !empty($title)) {
            $title = $t->translate($title);
        }
    }

    // get attribs for element
    $attribs = array(
        'id'     => $this->getIdAlias() . $page->getId(),
        'title'  => $title,
        'class'  => $page->getClass()
    );

    // does page have a href?
    if ($href = $page->getHref()) {
        $element = 'a';
        $attribs['href'] = $href;
        $attribs['target'] = $page->getTarget();
    } else {
        $element = 'span';
    }

    return '<' . $element . $this->_htmlAttribs($attribs) . '><span class="span-nav-icon"></span><span>'
         . str_replace(chr(32), '&nbsp;', $this->view->escape($label))
         . '</span></' . $element . '>';
}

/**
 * Normalizes given render options
 *
 * @param  array $options  [optional] options to normalize
 * @return array           normalized options
 */
protected function _normalizeOptions(array $options = array())
{
    if (isset($options['indent'])) {
        $options['indent'] = $this->_getWhitespace($options['indent']);
    } else {
        $options['indent'] = $this->getIndent();
    }

    if (isset($options['ulClass']) && $options['ulClass'] !== null) {
        $options['ulClass'] = (string) $options['ulClass'];
    } else {
        $options['ulClass'] = $this->getUlClass();
    }

    if (isset($options['liRole']) && $options['liRole'] !== null) {
        $options['liRole'] = (string) $options['liRole'];
    } else {
        $options['liRole'] = $this->getLiRole();
    }

    if (isset($options['idAlias']) && $options['idAlias'] !== null) {
        $options['idAlias'] = (string) $options['idAlias'];
    } else {
        $options['idAlias'] = '';
    }

    if (array_key_exists('minDepth', $options)) {
        if (null !== $options['minDepth']) {
            $options['minDepth'] = (int) $options['minDepth'];
        }
    } else {
        $options['minDepth'] = $this->getMinDepth();
    }

    if ($options['minDepth'] < 0 || $options['minDepth'] === null) {
        $options['minDepth'] = 0;
    }

    if (array_key_exists('maxDepth', $options)) {
        if (null !== $options['maxDepth']) {
            $options['maxDepth'] = (int) $options['maxDepth'];
        }
    } else {
        $options['maxDepth'] = $this->getMaxDepth();
    }

    if (!isset($options['onlyActiveBranch'])) {
        $options['onlyActiveBranch'] = $this->getOnlyActiveBranch();
    }

    if (!isset($options['renderParents'])) {
        $options['renderParents'] = $this->getRenderParents();
    }

    return $options;
}

 /**
 * Renders the deepest active menu within [$minDepth, $maxDeth], (called
 * from {@link renderMenu()})
 *
 * @param  Zend_Navigation_Container $container  container to render
 * @param  array                     $active     active page and depth
 * @param  string                    $ulClass    CSS class for first UL
 * @param  string                    $indent     initial indentation
 * @param  int|null                  $minDepth   minimum depth
 * @param  int|null                  $maxDepth   maximum depth
 * @return string                                rendered menu
 */
protected function _renderDeepestMenu(Zend_Navigation_Container $container,
                                      $ulClass,
                                      $indent,
                                      $minDepth,
                                      $maxDepth)
{
    if (!$active = $this->findActive($container, $minDepth - 1, $maxDepth)) {
        return '';
    }

    // special case if active page is one below minDepth
    if ($active['depth'] < $minDepth) {
        if (!$active['page']->hasPages()) {
            return '';
        }
    } else if (!$active['page']->hasPages()) {
        // found pages has no children; render siblings
        $active['page'] = $active['page']->getParent();
    } else if (is_int($maxDepth) && $active['depth'] +1 > $maxDepth) {
        // children are below max depth; render siblings
        $active['page'] = $active['page']->getParent();
    }

    $ulClass = $ulClass ? ' class="' . $ulClass . '"' : '';
    $html = $indent . '<ul' . $ulClass . '>' . self::EOL;

    $liRole = (! empty($this->getLiRole())) ? "role=\"{$this->getLiRole()}\"" : "";

    foreach ($active['page'] as $subPage) {
        if (!$this->accept($subPage)) {
            continue;
        }
        $liClass = $subPage->isActive(true) ? ' class="active"' : '';
        $html .= $indent . '    <li' . $liClass . ' ' . $liRole . '>' . self::EOL;
        $html .= $indent . '        ' . $this->htmlify($subPage) . self::EOL;
        $html .= $indent . '    </li>' . self::EOL;
    }

    $html .= $indent . '</ul>';

    return $html;
}

/**
 * Renders a normal menu (called from {@link renderMenu()})
 *
 * @param  Zend_Navigation_Container $container   container to render
 * @param  string                    $ulClass     CSS class for first UL
 * @param  string                    $indent      initial indentation
 * @param  int|null                  $minDepth    minimum depth
 * @param  int|null                  $maxDepth    maximum depth
 * @param  bool                      $onlyActive  render only active branch?
 * @return string
 */
protected function _renderMenu(Zend_Navigation_Container $container,
                               $ulClass,
                               $indent,
                               $minDepth,
                               $maxDepth,
                               $onlyActive)
{
    $html = '';

    // find deepest active
    if ($found = $this->findActive($container, $minDepth, $maxDepth)) {
        $foundPage = $found['page'];
        $foundDepth = $found['depth'];
    } else {
        $foundPage = null;
    }

    // create iterator
    $iterator = new RecursiveIteratorIterator($container,
                        RecursiveIteratorIterator::SELF_FIRST);
    if (is_int($maxDepth)) {
        $iterator->setMaxDepth($maxDepth);
    }

    // iterate container
    $prevDepth = -1;
    foreach ($iterator as $page) {
        $depth = $iterator->getDepth();
        $isActive = $page->isActive(true);
        if ($depth < $minDepth || !$this->accept($page)) {
            // page is below minDepth or not accepted by acl/visibilty
            continue;
        } else if ($onlyActive && !$isActive) {
            // page is not active itself, but might be in the active branch
            $accept = false;
            if ($foundPage) {
                if ($foundPage->hasPage($page)) {
                    // accept if page is a direct child of the active page
                    $accept = true;
                } else if ($foundPage->getParent()->hasPage($page)) {
                    // page is a sibling of the active page...
                    if (!$foundPage->hasPages() ||
                        is_int($maxDepth) && $foundDepth + 1 > $maxDepth) {
                        // accept if active page has no children, or the
                        // children are too deep to be rendered
                        $accept = true;
                    }
                }
            }

            if (!$accept) {
                continue;
            }
        }

        $liRole = (! empty($this->getLiRole())) ? "role=\"{$this->getLiRole()}\"" : "";


        // make sure indentation is correct
        $depth -= $minDepth;
        $myIndent = $indent . str_repeat('        ', $depth);

        if ($depth > $prevDepth) {
            // start new ul tag
            if ($ulClass && $depth ==  0) {
                $ulClass = ' class="' . $ulClass . '"';
            } else {
                $ulClass = '';
            }
            $html .= $myIndent . '<ul' . $ulClass . '>' . self::EOL;
        } else if ($prevDepth > $depth) {
            // close li/ul tags until we're at current depth
            for ($i = $prevDepth; $i > $depth; $i--) {
                $ind = $indent . str_repeat('        ', $i);
                $html .= $ind . '    </li>' . self::EOL;
                $html .= $ind . '</ul>' . self::EOL;
            }
            // close previous li tag
            $html .= $myIndent . '    </li>' . self::EOL;
        } else {
            // close previous li tag
            $html .= $myIndent . '    </li>' . self::EOL;
        }

        // render li tag and page
        $liClass = $isActive ? ' class="active"' : '';
        $html .= $myIndent . '    <li' . $liClass . ' ' . $liRole . '>' . self::EOL
               . $myIndent . '        ' . $this->htmlify($page) . self::EOL;

        // store as previous depth for next iteration
        $prevDepth = $depth;
    }

    if ($html) {
        // done iterating container; close open ul/li tags
        for ($i = $prevDepth+1; $i > 0; $i--) {
            $myIndent = $indent . str_repeat('        ', $i-1);
            $html .= $myIndent . '    </li>' . self::EOL
                   . $myIndent . '</ul>' . self::EOL;
        }
        $html = rtrim($html, self::EOL);
    }

    return $html;
}   

}

Admittedly a lot of code. Might be good to diff the class you have now against this http://pastebin.com/qiD2ULsz - and then seeing what the touch points are, creating a new extending class. Really just the new properties and some "tweaks" to the string concatenation it does to render the markup.

I don't specifically address class on "second level ul's" but passing in an additional property would be trivial and follow the same changes I did make.

Hope this helps some. ZF 1.x shows its age a bit and these view helpers were never that great. The underlying Nav code isn't too bad, so again, maybe just start from scratch and write your own code to render a Zend Nav Container. Good luck.

like image 138
ficuscr Avatar answered Oct 24 '22 00:10

ficuscr


This is admittedly an ugly hack, but you could do it by processing the output of public_nav_main() with a regular expression. So in the header.php file you would replace:

echo public_nav_main();

with

echo preg_replace( "/(?<!\/)ul(?!.*?nav)/", 'ul class="dropdown-menu"', public_nav_main() );

This will only work if you only have 2 levels in your menu, since the above regular expression will also add the class="dropdown-menu" to all ul elements below the top level ul.

Benefits

  • Simple
  • Achieves what you want to do
  • Doesn't require writing helpers or modifying the underlying framework
  • Should not break when the underlying framework is updated unless the update renders something other than a string from public_nav_main()

Downsides

  • Won't work if your menu has more than two levels
  • Will result in having two class attributes in your 2nd and lower level ul elements in the event that they already have a class attribute
like image 2
morphatic Avatar answered Oct 24 '22 01:10

morphatic