Below is a new version of the DropMenu snippet Ryan and I used to build all the menus for the MODx CMS site. Please take a look and see if you can find any bugs or might have any suggestions for improving this before we decide to include it as the default menu snippet in future releases.
Latest features include the addition of menutitle to list of available fields to use for link text or title, exclusion of documents not marked with the Show in Menu flag, configurable classes for top level and nested elements, optional DIV containers that can be used to create hover zones, and more.
Here are all the available snippet parameters:
$menuName [ string ] - Sets the name of the menu, including the placeholder, and top level DIV id (if topdiv option is true)
$startDoc [int] - The parent ID of your menu. Default 0: [[DropMenu?startDoc=10]]
$textOfLinks [ string ] - The database field you want the text of your links to be. The default is menutitle, but if you prefer it can be any of the following: id, pagetitle, description, parent, alias, longtitle, introtext
$removeNewLines [ true | false ] - If you want new lines removed from code. Default is false, But it is generally better for IE when lists are styled vertically if it is set to true
$levelLimit [ int ] - Maximum number of levels to include. The default 0 will allow all levels. Example: [[DropMenu?levelLimit=2]]
$showDescription [true | false] - Specify if you would like to include the description next to each link. Default is false
$titleOfLinks [ string ] - Field used to populate the link title attributes. The default is description, but if you prefer it can be any of the following: menutitle, id, pagetitle, description, parent, alias, longtitle, introtext
$selfAsLink [ true | false ] - Define if the current page should be a link (true) or not (false)
$topdiv [ true | false ] - Indicates if the top level UL is wrapped by a containing DIV block
$topdivClass [ string ] - CSS Class for DIV wrapping top level UL
$topnavClass [ string ] - CSS Class for top level UL
$subdiv [ true | false ] - Indicates if nested UL’s should be wrapped by containing DIV blocks
$subdivClass [ string ] - CSS Class for DIV blocks wrapping nested UL elements
$subnavClass [ string ] - CSS Class for nested UL’s
$hereClass [ string ] - CSS Class for LI and A when they are the currently selected page, as well as any ancestors of the current page (YOU ARE HERE)
$pre [ string ] - Text to append before links inside of LIs
$post [ string ] - Text to append before links inside of LIs
$orderBy [ string ] - Document field to sort menu by
$orderDesc [true | false] - Order results in descending order? Default is false
And here is the code (code is also in attachment, but note the PHP tags are in the file if you copy and paste the code into a snippet):
// ###########################################
// DropMenu #
// ###########################################
// Configurable menu / navigation builder using UL tags
// Offers optional DIV wrappers for top level and nested menus (useful for hover zones)
// as well as configurable classes for the DIV, UL, and LI elements. It even
// marks ancestors of and the current element with a hereClass (indicating you are here
// and in this area of the site). Also applies .last CSS class to final LI in each UL.
//
// Developed by Vertexworks.com and Opengeek.com
// Feel free to use if you keep this header and credits in place
//
// Inspired by List Site Map by Jaredc, SimpleList by Bravado,
// and ListMenuX by OpenGeek
// ###########################################
// Usage Examples #
// ###########################################
// Creates menu with wrapping DIV with id=myMenu, starting at the site root, two levels deep,
// with descriptions next to the links, and nested UL elements with class=nestedLinks; output
// of menu can be placed in layout using placeholder named myMenu ( e.g. [ +myMenu+ ] )
// [ [DropMenu?menuName=myMenu&startDoc=0&levelLimit=2&topdiv=true&showDescription=true&subnavClass=nestedLinks] ]
//
// Creates topMenu from site root, including only 1 level, with class=topMenubar applied to the top level UL
// and class=activeLink applied to current page LI
// [ [DropMenu?menuName=topMenu&startDoc=0&levelLimit=1&topnavClass=topMenubar&here=activeLink] ]
//
// Creates dropmenu 3 levels deep, with DIV wrappers around all nested lists styled with class=hoverZone
// and currentPage LI styled with class=currentPage
// [ [DropMenu?levelLimit=3&subdiv=true&subdivClass=hoverZone&subnavClass=menuZone&here=currentPage] ]
//
// Creates dropmenu of infinite levels, ordered by menutitle in descending order
// [ [DropMenu?orderBy=menutitle&orderDesc=true] ]
// ###########################################
// Configuration parameters #
// ###########################################
// $menuName [ string ]
// Sets the name of the menu, placeholder, and top level DIV id (if topdiv option is true)
$menuName = (empty($menuName)) ? 'dropmenu' : "$menuName";
// $siteMapRoot [int]
// The parent ID of your root. Default 0. Can be set in
// snippet call with startDoc (to doc id 10 for example):
// [[DropMenu?startDoc=10]]
$siteMapRoot = 0;
// $titleOfLinks [ string ]
// What database field do you want the title of your links to be?
// The default is pagetitle because it is always a valid (not empty)
// value, but if you prefer it can be any of the following:
// menutitle, id, pagetitle, description, parent, alias, longtitle, introtext
$textOfLinks = (empty($textOfLinks)) ? 'menutitle' : "$textOfLinks";
// $removeNewLines [ true | false ]
// If you want new lines removed from code, set to true. This is generally
// better for IE when lists are styled vertically.
$removeNewLines = (!isset($removeNewLines)) ? false : ($removeNewLines==true);
// $maxLevels [ int ]
// Maximum number of levels to include. The default 0 will allow all
// levels. Also settable with snippet variable levelLimit:
// [[DropMenu?levelLimit=2]]
$maxLevels = 0;
// $showDescription [true | false]
// Specify if you would like to include the description
// with the page title link.
$showDescription = (!isset($showDescription)) ? false : ($showDescription==true);
// $titleOfLinks [ string ]
// What database field do you want the title of your links to be?
// The default is pagetitle because it is always a valid (not empty)
// value, but if you prefer it can be any of the following:
// menutitle, id, pagetitle, description, parent, alias, longtitle, introtext
$titleOfLinks = (empty($titleOfLinks)) ? 'description' : "$titleOfLinks";
// $selfAsLink [ true | false ]
// Define if the current page should be a link (true) or not (false)
$selfAsLink = (!isset($selfAsLink)) ? false : ($selfAsLink==true);
// $topdiv [ true | false ]
// Indicates if the top level UL is wrapped by a containing DIV block
$topdiv = (!isset($topdiv)) ? false : ($topdiv==true);
// $topdivClass [ string ]
// CSS Class for DIV wrapping top level UL
$topdivClass = (empty($topdivClass)) ? 'topdiv' : "$topdivClass";
// $topnavClass [ string ]
// CSS Class for top level UL
$topnavClass = (empty($topnavClass)) ? 'topnav' : "$topnavClass";
// $subdiv [ true | false ]
// Indicates if nested UL's should be wrapped by containing DIV blocks
$subdiv = (!isset($subdiv)) ? false : ($subdiv==true);
// $subdivClass [ string ]
// CSS Class for DIV blocks wrapping nested UL elements
$subdivClass = (empty($subdivClass)) ? 'subdiv' : "$subdivClass";
// $subnavClass [ string ]
// CSS Class for nested UL's
$subnavClass = (empty($subnavClass)) ? 'subnav' : "$subnavClass";
// $hereClass [ string ]
// CSS Class for LI and A when they are the currently selected page, as well
// as any ancestors of the current page (YOU ARE HERE)
$hereClass = (empty($hereClass)) ? 'here' : $hereClass;
// $pre [ string ]
// Text to append before links inside of LIs
$pre = (empty($pre)) ? '' : "$pre";
// $post [ string ]
// Text to append before links inside of LIs
$post = (empty($post)) ? '' : "$post";
// $orderBy [ string ]
// Document field to sort menu by
$orderBy = (empty($orderBy)) ? 'menuindex' : "$orderBy";
// $orderDesc [true | false]
// Order results in descending order? default is false
$orderDesc = (!isset($orderDesc)) ? false : ($orderDesc==true);
// ###########################################
// End config, the rest takes care of itself #
// ###########################################
// Initialize
$MakeMap = "";
$siteMapRoot = (isset ($startDoc)) ? $startDoc : $siteMapRoot;
$maxLevels = (isset ($levelLimit)) ? $levelLimit : $maxLevels;
$ie = ($removeNewLines) ? '' : "\n";
// Overcome single use limitation on functions
global $MakeMap_Defined;
if (!isset ($MakeMap_Defined)) {
function filterHidden($var) {
return (!$var['hidemenu']==1);
}
function MakeMap($callBy, $listParent, $listLevel, $description, $titleOfLinks, $maxLevels, $inside, $pre, $post, $selfAsLink, $ie, $activeLinkIDs, $topdiv, $topdivClass, $topnavClass, $subdiv, $subdivClass, $subnavClass, $hereClass, $textOfLinks, $orderBy, $orderDesc) {
$children = $callBy->getActiveChildren($listParent, $orderBy, (!$orderDesc) ? 'ASC' : 'DESC', 'id, pagetitle, description, isfolder, parent, alias, longtitle, menutitle, hidemenu, introtext, content_dispo, contentType, type');
if (is_array($children) && !empty ($children)) {
$children = array_filter($children, "filterHidden");
$numChildren = count($children);
// determine if it's a top category or not
$toplevel = !$inside;
$topdivcls = (!empty ($topdivClass)) ? ' class="'.$topdivClass.'"' : '';
$topdivblk = ($topdiv) ? "<div$topdivcls>" : '';
$topnavcls = (!empty ($topnavClass)) ? ' class="'.$topnavClass.'"' : '';
$subdivcls = (!empty ($subdivClass)) ? ' class="'.$subdivClass.'"' : '';
$subdivblk = ($subdiv) ? "<div$subdivcls>$ie" : '';
$subnavcls = (!empty ($subnavClass)) ? ' class="'.$subnavClass.'"' : '';
$output = ($toplevel) ? "$topdivblk<ul$topnavcls>$ie" : "$ie$subdivblk<ul$subnavcls>$ie";
//loop through and process subchildren
foreach ($children as $child) {
// figure out if it's a containing category folder or not
$numChildren --;
$isFolder = $child['isfolder'];
$itm = "";
if (!$inside) {
$itm .= (!$selfAsLink && ($child['id'] == $callBy->documentIdentifier)) ? $pre.$child[$textOfLinks].$post : '<a href="[~'.$child['id'].'~]" title="'.$child[$titleOfLinks].'">'.$pre.$child[$textOfLinks].$post.'</a>';
}
elseif ($isFolder && $inside) {
$itm .= '<a href="[~'.$child['id'].'~]" title="'.$child[$titleOfLinks].'">'.$child[$textOfLinks].'</a>';
} else {
$itm .= ($child['alias'] > '0') ? '<a href="[~'.$child['id'].'~]" title="'.$child[$titleOfLinks].'">'.$child[$textOfLinks].'</a>' : '<span>'.$child[$textOfLinks].'</span>';
}
// loop back through if the doc is a folder and has not reached the max levels
if ($isFolder && (($maxLevels == 0) || ($maxLevels > $listLevel +1))) {
$itm .= MakeMap($callBy, $child['id'], $listLevel +1, $description, $titleOfLinks, $maxLevels, true, $pre, $post, $selfAsLink, $ie, $activeLinkIDs, $topdiv, $topdivClass, $topnavClass, $subdiv, $subdivClass, $subnavClass, $hereClass, $textOfLinks, $orderBy, $orderDesc);
}
if ($itm && !$selfAsLink && ($child['id'] == $callBy->documentIdentifier)) {
$output .= " <li class=\"$hereClass". ($numChildren == 0 ? ' last' : '')."\">$itm</li>$ie";
}
elseif ($itm) {
if ($numChildren == 0) {
$class = 'last';
}
if (is_array($activeLinkIDs)) {
if (in_array($child['id'], $activeLinkIDs)) {
$class .= ($class ? ' ' : '').'here';
}
}
if ($class) {
$class = ' class="'.$class.'"';
}
$output .= "<li$class>$itm</li>$ie";
$class = '';
}
}
$output .= "</ul>$ie";
$output .= ($toplevel) ? (($topdiv) ? "</div>$ie" : "") : (($subdiv) ? "</div>$ie" : "");
}
return $output;
}
$MakeMap_Defined = true;
}
$currentID = $modx->documentIdentifier;
$parentID = $currentID;
// find the parent docs of the current "you-are-here" doc
// used in the logic to mark parents as such also
while ($parentID != $siteMapRoot && $parentID != 0) {
$parent = $modx->getParent($parentID, 0);
if ($parent) {
$parentID = $parent['id'];
$activeLinkIDs[] = $parentID;
} else {
$parentID = 0;
}
}
$modx->setPlaceholder($menuName, MakeMap($modx, $siteMapRoot, 0, $showDescription, $titleOfLinks, $maxLevels, false, $pre, $post, $selfAsLink, $ie, $activeLinkIDs, $topdiv, $topdivClass, $topnavClass, $subdiv, $subdivClass, $subnavClass, $hereClass, $textOfLinks, $orderBy, $orderDesc));
Note that I made this version use a placeholder to assign the menu output, but we can easily make this optional, so it will return the output directly from the snippet or via a placeholder, depending on user preferences.
I’ll release this and prepare it for inclusion in the installer for future versions once I get a little feedback on it.
Cheers...