I have a custom two level menu in WordPress. There is an upper level and when you hover over the items, a submenu appears. Two menu items in the submenu have a button that is not in the other submenus. These two paragraphs have a "browse all" class. I need to check this class in Walker_Nav_Menu and add a custom button to the submenu. How can I check for class "browse all"?
In my code I'm creating a wrapper for ul.sub-menu. I need to check if there is a "browse all" class in the element in order to add a button to this wrapper. Such a button will only be in items with the "browse all" class.
class My_Walker extends Walker_Nav_Menu {
function start_lvl( & $output, $depth = 0, $args = array()) {
$indent = str_repeat("\t", $depth);
if ($depth == 0) {
$output. = "\n$indent<div class='sub-menu__depth-1'><ul class='sub-menu sub-menu__main'>\n";
} else {
$output. = "\n$indent<ul class='sub-menu'>\n";
}
}
function end_lvl( & $output, $depth = 0, $args = array()) {
$indent = str_repeat("\t", $depth);
if ($depth == 0) {
$output. = "$indent</ul> <
/div>\n";
} else {
$output. = "$indent</ul>\n";
}
}
}
Below is an example of how it should be:
class My_Walker extends Walker_Nav_Menu
{
function start_lvl(&$output, $depth = 0, $args = array())
{
$indent = str_repeat("\t", $depth);
if ($depth == 0) {
$output .= "\n$indent<div class='sub-menu__depth-1'><ul class='sub-menu sub-menu__main'>\n";
} else {
$output .= "\n$indent<ul class='sub-menu'>\n";
}
}
function end_lvl(&$output, $depth = 0, $args = array())
{
$indent = str_repeat("\t", $depth);
if ($depth == 0) {
//here, a button appears outside the sub-menu if the navigation element has the class "browse all"
$output .= "$indent</ul>
<a>Browse All</a>
</div>\n";
} else {
$output .= "$indent</ul>\n";
}
}
}
I don't think you can do that directly in start_lvl or end_level - because those only create the wrapping UL. But that one doesn't have the classes you are looking for, those are on the actual navigation items, and those are processed/rendered in start_el and end_el.
But I suppose you could add a property to the class, an array - and in that one you keep the info, whether the current (sub-)menu of the specific depth requires these additional elements to be added, or not.
class My_Walker extends Walker_Nav_Menu {
private $needsCustomButtons = [];
function start_lvl( & $output, $depth = 0, $args = array()) {
$this->needsCustomButtons[$depth] = false;
// rest of the stuff that needs doing here
}
public function start_el( &$output, $data_object, $depth = 0, $args = null, $current_object_id = 0 ) {
// stuff
$classes = empty( $menu_item->classes ) ? array() : (array) $menu_item->classes;
$classes[] = 'menu-item-' . $menu_item->ID;
if(in_array('browse-all', $classes)) {
$this->needsCustomButtons[$depth] = true;
}
// more stuff here
}
function end_lvl( & $output, $depth = 0, $args = array()) {
// if $this->needsCustomButtons[$depth] is true here, then you know
// your two extra nav items need adding, so concatenate them to
// $output here, before the closing `</li>` tag gets added.
}
}
Related
Well, I'm trying to get the description of a menu item and of a sub menu with the code attached below, but I'm not able to get it.
I'm trying to get the description of "About us" and the description of "Our board staff":
For the menu item (About us) is working good but for some reason the sub menu (Our board and staff) doesn't contain the information description and it just have ID, URL and Title, I already tried a var_dump() of the sub menu object (as you can see it in the code below) but it doesn't has it.
function get_menu_section_description($sectionUrl){
$menu = wp_get_menu_array("menu");
$desc = "";
foreach ($menu as $key => $item){
$arr = $item['url'];
// var_dump($item);
if ($sectionUrl == $arr[0]) {
$desc = $item['description'];
}
if(sizeof($item['children']) > 0){
foreach ($item['children'] as $key => $children){
// var_dump($children);
$arr2 = $children['url'];
if ($sectionUrl == $arr2) {
$desc = $children['description'];
}
}
} } return $desc; }
Anyone know why doesn't have the description item, how to activate it or a possible solution for that? Thanks in advance.
Since WordPress 3.0, you don't need a custom walker anymore!
There is the walker_nav_menu_start_el filter, see https://developer.wordpress.org/reference/hooks/walker_nav_menu_start_el/
Example
function add_menu_description($item_output, $item, $depth, $args) {
if (strlen($item->description) > 0 ) {
// append description after link
$item_output .= sprintf('<span class="description">%s</span>', esc_html($item->description));
// insert description as last item *in* link ($input_output ends with "</a>{$args->after}")
//$item_output = substr($item_output, 0, -strlen("</a>{$args->after}")) . sprintf('<span class="description">%s</span >', esc_html($item->description)) . "</a>{$args->after}";
}
return $item_output;
}
add_filter('walker_nav_menu_start_el', 'add_menu_description', 10, 4);
I found a solution few days a go, so may it help someone, the problem I had was was the function to call the menu wp_get_menu_array(), there I had to add the description in the sub menu, just that:
function wp_get_menu_array($current_menu) {
$array_menu = wp_get_nav_menu_items($current_menu);
$menu = array();
foreach ($array_menu as $m) {
if (empty($m->menu_item_parent)) {
$menu[$m->ID] = array();
$menu[$m->ID]['ID'] = $m->ID;
$menu[$m->ID]['title'] = $m->title;
$menu[$m->ID]['url'] = $m->url;
$menu[$m->ID]['classes'] = $m->classes;
$menu[$m->ID]['description'] = $m->description;
$menu[$m->ID]['children'] = array();
}
}
$submenu = array();
foreach ($array_menu as $m) {
if ($m->menu_item_parent) {
$submenu[$m->ID] = array();
$submenu[$m->ID]['ID'] = $m->ID;
$submenu[$m->ID]['title'] = $m->title;
$submenu[$m->ID]['url'] = $m->url;
$submenu[$m->ID]['description'] = $m->description; //Line added;
$menu[$m->menu_item_parent]['children'][$m->ID] = $submenu[$m->ID];
}
}
return $menu;
}
I have a table menu like this
i have problem to display like this with ul and li tag like 2nd picture.
please help me for the solution
menu_code|desc_code
1 | menu 1
1.1 | menu 1.1
1.2 | menu 1.2
1.2.1| menu 1.2.1
2 | menu 2
i want to display my table menu with concept "unlimited level menu".
I would consider changing your table structure. You will need to loop through each parent and children, you wouldnt want to do that with string splitting. I advice you to create an extra column parent_id and binding your items like that. After that its easy to crawl recursively through its children to create a ul > li structure.
Example recursive function:
public static function buildTree($items, $parent_id = null) {
$result = [];
foreach($items as $item) {
if($item->parent_id == $parent_id) {
$children = self::buildTree($items, $item->id);
if($children) {
$item->children = $children;
}
$result[$item->id] = $item;
}
}
return $result;
}
After that you can use the result of the build function to recursively create your menu structure:
public static function treeToHtml($tree, $level = 0) {
$result = '';
$result .= '<ul>';
foreach($tree as $item) {
$has_children = isset($item->children) && count($item->children) > 0;
if($has_children) {
$result .= '<li><a href="#">';
$result .= self::treeToHtml($item->children, $level + 1);
$result .= '</li>';
} else {
$result .= '<li><a href="#"></li>';
}
}
$result .= '</ul>';
return $result;
}
Edit function accordingly and statically all depends on the context you're using it in.
I'm trying to create a series of <ul> and <li> to create a directory/file structure to navigate some files created from a table in a dB. The table (tb_lib_manual) contains both files and folders.
If a record has a null entry for fileID then it is a folder not a file. each record has a parentID to show which folder is parent, for files and folders in the root this is 0.
The php code is thus:
class library_folders extends system_pageElement
{
private $html = '';
private $i = 0;
private $stmtArray = array();
private $objectArray = array();
function __construct()
{
parent::__construct();
$this->nextList();
}
function nextList($parentID = 0)
{
$qSQL = 'SELECT * FROM tb_lib_manual WHERE parentID=:parentID';
$stmtArray[$this->i] = $this->dbConnection->prepare($qSQL);
$stmtArray[$this->i]->bindValue(':parentID', $parentID, PDO::PARAM_INT);
$stmtArray[$this->i]->execute();
if($stmtArray[$this->i]->rowCount() > 0)
{
$display ='';
if($parentID != 0)
{
$display = ' style="display:none"';
}
$this->html .= '<ul' . $display . '>';
}
while ($this->objectArray[$this->i] = $stmtArray[$this->i]->fetchObject())
{
$this->html .= '<li>' . $this->objectArray[$this->i]->title;
if($this->objectArray[$this->i]->fileID == null)
{
//we have a folder!
$manualID = $this->objectArray[$this->i]->manualID;
$this->i ++;
$this->nextList($manualID);
$this->i--;
}
$this->html .= '</li>';
}
if($stmtArray[$this->i]->rowCount() > 0)
{
$this->html .= '</ul>';
}
echo $this->html;
}
function __destruct()
{
parent::__destruct();
}
}
The problem is when the code returns back to the while loop after calling itself it restarts the loop rather than carrying on where it left off, causing a repeat in the child folder. Is there either a better way to do this or am I doing something wrong!?
Table looks like this:
output like this:
'A ManualA ManualFolder 1Nasa exerciseA ManualA ManualFolder 1Nasa exercise'
Fixed the code, very silly mistake, echo in the wrong place...
class library_folders extends system_pageElement
{
private $html = '';
private $i = 0;
private $stmtArray = array();
private $objectArray = array();
function __construct()
{
parent::__construct();
$this->nextList();
echo $this->html;
}
function nextList($parentID = 0)
{
$qSQL = 'SELECT * FROM tb_lib_manual WHERE parentID=:parentID';
//echo $this->i;
$stmtArray[$this->i] = $this->dbConnection->prepare($qSQL);
$stmtArray[$this->i]->bindValue(':parentID', $parentID, PDO::PARAM_INT);
$stmtArray[$this->i]->execute();
if($stmtArray[$this->i]->rowCount() > 0)
{
$this->html .= '<ul>';
}
while ($this->objectArray[$this->i] = $stmtArray[$this->i]->fetchObject())
{
$this->html .= '<li>' . $this->objectArray[$this->i]->title;
if($this->objectArray[$this->i]->fileID == null)
{
//we have a folder!
$manualID = $this->objectArray[$this->i]->manualID;
$this->i ++;
$this->nextList($manualID);
$this->i--;
}
$this->html .= '</li>';
}
if($stmtArray[$this->i]->rowCount() > 0)
{
$this->html .= '</ul>';
}
}
function __destruct()
{
parent::__destruct();
}
}
Yes, there are better ways of doing this.
If you fetch the whole tree every time, you might as well load all the nodes into a big array (of objects), put each node's id as index and then loop over the array once to create the parent references by for example
// create a fake root node to start your traversal later on
// only do this if you don't have a real root node
// which I assume you don't
$root = (object) ["children" => []];
// loop over all nodes
foreach ($nodes as $node)
{
// if the node has a parent node that is not root
if ($node->parentId > 0)
{
// put it into it's parent's list of children
$nodes[ $node->parentId ]->children[] = $node;
}
else
{
// otherwise put it into root's list of children
$root->children[] = $node;
}
}
Complexity: You do one query and you have to iterate all your nodes once.
For this to work your nodes need to be objects. Otherwise each assignment to $node->children will create a copy of the assigned node where you wanted a reference.
If you do not want to fetch all nodes, you can go through your tree level by level by creating a list of node ids from the previous level.
function fetchLevel ($nodes, $previousLevelIds)
{
// get all children from the previous level's nodes
// I assume $previousLevelIds to be an array of integers. beware of sql injection
// I refrained from using prepared statements for simplicity
$stmt = $pdo->query("SELECT id, parentId FROM nodes WHERE parentId IN (".implode(",",$previousLevelIds).")");
// fetch nodes as instances of stdclass
$currentLevelNodes = $stmt->fetchAll(PDO::FETCH_OBJ);
$nextLevelIds = [];
foreach ($currentLevelNodes as $node)
{
// ids for next level
$nextLevelIds[] = $node->id;
// parent <-> child reference
$nodes[ $node->parentId ]->children[] = $node;
}
// fetch the next level only if we found any nodes in this level
// this will stop the recursion
if ($nextLevelIds)
fetchLevel($nodes, $nextLevelIds);
}
// start by using a fake root again
$root = (object) ["id" => 0, "children" => []];
$nodes = [0 => $root];
fetchLevel($nodes, [0]);
// or start with a specific node in the tree
$node = $pdo->query("SELECT id, parentId FROM nodes WHERE id = 1337")->fetch(PDO::FETCH_OBJ);
$nodes = [$node->id => $node];
fetchLevel($nodes, [$node->id]);
// or a number of nodes which don't even have to
// be on the same level, but you might fetch nodes multiple times
// if you it this way
Complexity: Number of queries <= height of your tree. You only iterate each fetched node once.
For displaying the tree as html list you iterate once more:
class Foo {
public $html;
public function getList ($nodes)
{
// outer most ul
$this->html = '<ul>';
$this->recurseList($nodes);
$this->html .= '</ul>';
}
protected function recurseList ($nodes)
{
foreach ($nodes as $node)
{
$this->html .= "<li><span>".$node->name."</span>";
if ($node->children)
{
if ($node->parentId > 0)
$this->html .= '<ul style="display:none">';
else
$this->html .= '<ul>';
$this->recurseList($node->children);
$this->html .= "</ul>";
}
$this->html .= "</li>";
}
}
}
Some unrelated remarks:
instead of a hard-coded style="display:none" you could just use a css rule like ul li ul {display:none} to hide all lists below root
I split fetching the data from the database and converting the data for displaying into two separate scripts, which is the standard when you develop using MVC. If you don't want to do this run the scripts in succession.
PHP itself is a template engine, but consider using another template engine to display your html like Smarty. I personally prefer smarty for it's simpler syntax.
stackoverflow answer on how to bind a PHP array to a MySQL IN-operator using prepared statements
if you need to fetch subtrees often consider using a special table structure to reduce the number of queries
i created custom helper to build dynamic menu and i need to use this helper in all my site pages so i put the code to show menu in element and include it in default.ctp like this
<?php echo $this->element('menu'); ?>
,, but the default.ctp not defined the helper so how to define this helper in all views it give me this errors
Notice (8): Undefined variable: data [APP\views\elements\menu.ctp, line 5]
Warning (2): Invalid argument supplied for foreach() [APP\views\helpers\tree.php, line 28]
helpers/tree.php
<?php
class TreeHelper extends Helper
{
var $tab = " ";
var $helpers = array('Html');
// Main Function
function show($name, $data, $style='')
{
list($modelName, $fieldName) = explode('/', $name);
if ($style=='options') {
$output = $this->selecttag_options_array($data, $modelName, $fieldName, $style, 0);
} else {
//$style='';
$output = $this->list_element($data, $modelName, $fieldName, $style, 0);
}
return $this->output($output);
}
// This creates a list with optional links attached to it
function list_element($data, $modelName, $fieldName, $style, $level)
{
$tabs = "\n" . str_repeat($this->tab, $level * 2);
$li_tabs = $tabs . $this->tab;
$output = $tabs. "<ul>";
foreach ($data as $key=>$val)
{
$output .= $li_tabs . "<li>".$this->style_print_item($val[$modelName], $modelName, $style);
if(isset($val['children'][0]))
{
$output .= $this->list_element($val['children'], $modelName, $fieldName, $style, $level+1);
$output .= $li_tabs . "</li>";
}
else
{
$output .= "</li>";
}
}
$output .= $tabs . "</ul>";
return $output;
}
// this handles the formatting of the links if there necessary
function style_print_item($item, $modelName, $style='')
{
switch ($style)
{
case "link":
$output = $this->Html->link($item['name'], "view/".$item['id']);
break;
case "admin":
$output = $item['name'];
$output .= $this->Html->link(" edit", "edit/".$item['id']);
$output .= " ";
$output .= $this->Html->link(" del", "delete/".$item['id']);
break;
default:
$output = $item['name'];
}
return $output;
}
// recursively reduces deep arrays to single-dimensional arrays
// $preserve_keys: (0=>never, 1=>strings, 2=>always)
// Source: http://php.net/manual/en/function.array-values.php#77671
function array_flatten($array, $preserve_keys = 1, &$newArray = Array())
{
foreach ($array as $key => $child)
{
if (is_array($child))
{
$newArray =& $this->array_flatten($child, $preserve_keys, $newArray);
}
elseif ($preserve_keys + is_string($key) > 1)
{
$newArray[$key] = $child;
}
else
{
$newArray[] = $child;
}
}
return $newArray;
}
// for formatting selecttag options into an associative array (id, name)
function selecttag_options_array($data, $modelName, $fieldName, $style, $level)
{
// html code does not work here
// tried using " " and it didn't work
$tabs = "-";
foreach ($data as $key=>$val)
{
$output[] = array($val[$modelName]['id'] => str_repeat($tabs, $level*2) . ' ' . $val[$modelName]['name']);
if(isset($val['children'][0]))
{
$output[] = $this->selecttag_options_array($val['children'], $modelName, $fieldName, $style, $level+1);
}
}
$output = $this->array_flatten($output, 2);
return $output;
}
}
?>
elements/menu.ctp
<!-- This will turn the section name into a link -->
<h3>Basic hierarchical list with name as link</h3>
<?php echo $tree->show('Section/name', $data, 'link'); ?>
You have to define it in your AppController, located in the root of your app directory (and if it's not there, just create a file called app_controller.php You can use the file with the same name in the Cake core directory as a template for this file.
When you have your app_controller, add the following
var $helpers = array('Tree');
You might want to add some other standard helpers like Html, Form and Javascript in here as well. All the helpers that are in AppController will be available to all of your controllers.
i record number of queries of my website and in page the below script runs , 40 extra queries added to page .
how can I change this sql connection into a propper and light one
function tree_set($index)
{
//global $menu; Remove this.
$q=mysql_query("select id,name,parent from cats where parent='$index'");
if(mysql_num_rows($q) === 0)
{
return;
}
// User $tree instead of the $menu global as this way there shouldn't be any data duplication
$tree = $index > 0 ? '<ul>' : ''; // If we are on index 0 then we don't need the enclosing ul
while($arr=mysql_fetch_assoc($q))
{
$subFileCount=mysql_query("select id,name,parent from cats where parent='{$arr['id']}'");
if(mysql_num_rows($subFileCount) > 0)
{
$class = 'folder';
}
else
{
$class = 'file';
}
$tree .= '<li>';
$tree .= '<span class="'.$class.'">'.$arr['name'].'</span>';
$tree .=tree_set("".$arr['id']."");
$tree .= '</li>'."\n";
}
$tree .= $index > 0 ? '</ul>' : ''; // If we are on index 0 then we don't need the enclosing ul
return $tree;
}
//variable $menu must be defined before the function call
$menu = '....<ul id="browser" class="filetree">'."\n";
$menu .= tree_set(0);
$menu .= '</ul>';
echo $menu;
i heard , this can be done by changing it into an array , but i don't know how to do so
thanks in advance
Try this (untested code):
function tree_set($index)
{
//global $menu; Remove this.
$q=mysql_query("select id,name,parent from cats where parent='$index'");
if(mysql_num_rows($q) === 0)
return;
$cats = array();
$cat_ids = array();
while($arr=mysql_fetch_assoc($q))
{
$id = intval($arr['id']);
$cats[$id] = $arr;
}
$subFilesCountQuery="select parent,count(*) as subFileCount from cats where parent=".
join(" OR parent=",array_keys($cats))." GROUP BY parent";
$subFileCountResult=mysql_query($subFilesCountQuery);
while($arr=mysql_fetch_assoc($subFileCountResult))
{
$id = intval($arr['parent']);
$cats[$id]['subFileCount'] = $arr['subFileCount'];
}
// If we are on index 0 then we don't need the enclosing ul
$tree = $index > 0 ? '<ul>' : '';
foreach($cats as $id => $cat)
{
if($cat['subFileCount'] > 0)
$class = 'folder';
else
$class = 'file';
$tree .= '<li>';
$tree .= '<span class="'.$class.'">'.$arr['name'].'</span>';
$tree .=tree_set("".$arr['id']."");
$tree .= '</li>'."\n";
}
$tree .= $index > 0 ? '</ul>' : '';
What I'm doing is two queries: One to fetch all the categories (your original first query) followed by a second query to fetch all the subcategory counts in one fell swoop. I am also storing all categories in an array which you can loop through, rather than displaying as you fetch from the database.
It can be done by copying your data out into an array, and then using that copy: i.e.
while($arr=mysql_fetch_assoc($q))
{
$results[] = $arr;
}
later on, you then do whatever op you want on $results
The main problem with your code is you are mixing your display logic all in with your SQL query.
Select whole tree in single query, like "select id,name,parent from cats". Iterate on result, build array in PHP that will represent your tree, then draw HTML using array as source