I am now dealing with the same problem.
And i am using events for that. Listener example:
Event::listen('admin.menu.build', function($menu)
{
$menu->add('dashboard');
$menu->add('users');
$menu->add('users.profile');
});
Maybe should test and add few dozens more listeners. But still, if it will be slow, i will think of caching mechanism for that.
Add method accepts two parameters
My routes are named the same as menu path, for you maybe add third param for route name, or url.
With config i think it would be more work to do (both you and app) to collect those items.
I guess with this approach I find that it can be quite restrictive.
For example I might have a global.php
Event::listen('admin.menu.build', function($menu)
{
$menu->add('blog,index', 'Blog')
->add('blog.add', 'Add Blog');
});
Then in each module in the ServiceProvider::boot function have the same.
This would probably work but customisation is restrictive. Also organising the modules in order can be restrictive as well. How do you get around that?
Now they are showing as registered, but ordering is on my to do list.
I think one more param for add method will do the trick. But i am thinking of custom icons for each menu item and so on.
So in time i will create seperate class that will be created for each menu item and final result i am aiming to is something like this:
Event::listen('admin.menu.build', function(MenuBuilderInterface $menu)
{
$menu->add('dashboard', 'Blog')->order(4)->icon('dashboard');
$menu->add('user.profile')->first()->icon('user');
});
As icons will be used font awesome (as it is now).
But this is how i am trying to do, its not even done jet. So it isn't tested and 100% best solution, maybe you will come up with something better for your purposes.
Side note: i think that this type of architecture should be as restrictive as possible. And listeners shouldn't have much freedom on how to implement stuff. One interface to depend on and thats it.
Just having a play around and whipped up something fast and ugly but basically this could work and you can also extend menus from modules. Just not sure if this would be overkill or really slow.
Event::listen('admin.menu.create', function()
{
return array(
'icon' => '[icon destination]',
'title' => 'Settings',
'event' => 'admin.menu.settings.extend',
'menu' => array(
'General' => '[url]',
'Blog' => '[url]'
)
);
});
Event::listen('admin.menu.create', function()
{
return array(
'icon' => '[icon destination]',
'title' => 'Blog',
'event' => 'admin.menu.blog.extend',
'menu' => array(
'All Posts' => '[url]',
'Add New' => '[url]',
'Categories' => '[url]'
)
);
}, 1);
Event::listen('admin.menu.settings.extend', function()
{
return array(
'Server' => '[url]',
'This' => '[url]'
);
});
?>
Then basically just build out HTML like
<?php $menu = Event::fire('admin.menu.create'); ?>
<ul>
<?php foreach ($menu as $item): ?>
<li>
<?php echo $item['icon']; ?> - <?php echo $item['title']; ?>
<?php
$extended = current(Event::fire($item['event']));
if ($extended) { $item['menu'] = array_merge($item['menu'], $extended); }
?>
<ul>
<?php foreach ($item['menu'] as $key => $value): ?>
<li><?php echo $key; ?> - <?php echo $value; ?></li>
<?php endforeach; ?>
</ul>
</li>
<?php endforeach; ?>
</ul>
Returning arrays will work fine, but now if someday you will want to change something in this event you basicly should go through each listener as well.
Using my approach you will only need to change one class.
About nested listeners, it think that would be overkill, for such task. Because it isn't even a primary content...
The problem about the nested listenering is that sometimes I might need to extend Settings and add more links under the area even if its coming from a random Module.
Can you do that with your system at all?
Also even if I abstract them into a Menu Class and change something which is a core part it is still going to require a fair bit of customisation.
Not too sure the best way to handle this in a nice cleanly fashion.
Also there will probably only be 10-20 listeners maximum. I wonder if that could be complete overkill.
Can you do that with your system at all? Yes it can
Event::listen('admin.menu.build', function($menu)
{
$menu->add('dashboard');
$menu->add('users');
$menu->add('users.profile');
});
1st argument passed is path in menu. So first two will be top level menu items and 3rd one will be nested under users. So i can create, for example, users groups module and create menu builder listener:
Event::listen('admin.menu.build', function($menu)
{
$menu->add('users.groups');
});
And there will be added one more item under users top menu.
Or create profile edit page, that in menu will be nested in 3rd level under users.profile
Event::listen('admin.menu.build', function($menu)
{
$menu->add('users.profile.edit', 'Edit acount');
});
One realy big drawback for events is, that they arent IDE friendly (so no autosugestion, error handling if wrong data passed and so on). You have to know exacly how each of them are named, + different returns from listeners and sublisteners. If you work alone on project, than you know ins and outs of it, and it isnt a big deal.
I on the other hand, need to come up with solution, that would be friendly for many developers. So i even created event listener abstraction method. So for end user it would look more like this:
Admin::menu(function($menu) {
$menu->add('users');
$menu->add('users.profile');
});
Ahh ok the way you have built yours is actually really good. Just looking at it some more and the Abstraction is a lot nicer.
Curious don't support you have any sample code or your menu class you can show at all? Mainly how you look and go over the keys?
Maybe someday i will publish a package..
This is simple abstraction ( Admin::menu() ):
public function menu( $closure )
{
$this->events->listen('admin.menu.build', $closure );
}
I am using Navigation class that extends Illuminate\Support\Collection. So some handy collection handling is added.
public function add($path, $name = null, $url = null, $order = null, $icon = null)
{
$item = [
// This gets added to html, for developers to easy find where to hook
'path' => $path,
// This gets name, passed one, or from translations using $path
'name' => $this->getAlais($name, $path),
// This gets url, passed one, or gets named route from $path
'href' => $this->getHref($url, $path),
// This gets icon, passed one, or uses default fallback
'icon' => $this->getIcon($icon),
'order' => $order,
// Empty array, just to in views could do if($item['children']), insted of if(is_set($item['children']))
'children' => []
];
// This allows each dotnation be nested in children array
$nestedPath = str_replace('.', '.children.', $path);
// Laravel is great to deal with dots and arrays :)
array_set($this->items, $nestedPath, $item);
}
It would produce something like:
[
'dashboard' => [
'path' => [path],
'name' => [name],
'href' => [href],
'icon' => [icon],
'order' => [order],
'children' => [],
],
'users' => [
'path' => [path],
'name' => [name],
'href' => [href],
'icon' => [icon],
'order' => [order],
'children' => [
'profile' => [
'path' => [path],
'name' => [name],
'href' => [href],
'icon' => [icon],
'order' => [order],
'children' => [
'edit' => [
'path' => [path],
'name' => [name],
'href' => [href],
'icon' => [icon],
'order' => [order],
'children' => []
],
],
],
'new' => [
'path' => [path],
'name' => [name],
'href' => [href],
'icon' => [icon],
'order' => [order],
'children' => [],
],
],
],
]
So now in your view iterate trough array, show item, if has children, iterate trough them, and so on, you get the point.
Hope it will help. :)
One thing that i am currently working on, is how to set parent as active, if one of its childrens are active (so it would be by default opened, not collapsed)
Yeah that is an interesting one. Maybe I should try and create my own half baked implementation and then we can share idea's.
revati, I really liked your approach :) very nice indeed. To give my approach to the problem you mentioned about making menu item active, I use javascript :)
/*
* making meniu active
*/
var menuLinks = $(".nav-list > li > a");
$.each(menuLinks, function(){
var url = $(this).attr("href");
var urlRegex = new RegExp(url + ".*","i");
if(document.URL.match(urlRegex) != null && url != "#"){
$(this).parent().addClass("active");
}
});
var subMenuLinks = $(".nav-list > li > ul > li > a");
$.each(subMenuLinks, function(){
var url = $(this).attr("href").replace("#", "");
var urlRegex = new RegExp(url + ".*","i");
if(document.URL.match(urlRegex) != null && url != "#"){
$(this).parent().addClass("active");
$(this).parent().parent().css({display: "block"});
$(this).parent().parent().parent().addClass("active").addClass("open");
}
});
joshbenham said:
Yeah that is an interesting one. Maybe I should try and create my own half baked implementation and then we can share idea's.
Yeay, that would be great, i almost finished migrating to version where each menu item is separate NavigationItem instance.
luknei said:
revati, I really liked your approach :) very nice indeed. To give my approach to the problem you mentioned about making menu item active, I use javascript :)
It would be much easier, but i need to come up with a solution witch works 100% without js.
revati said: It would be much easier, but i need to come up with a solution witch works 100% without js.
Actually you can implement pretty much the same logic in php, preg_mach menu items URL with the current URL, and just climb up to in the array of menu items and set them as active
revati said:
joshbenham said:
Yeah that is an interesting one. Maybe I should try and create my own half baked implementation and then we can share idea's.
Yeay, that would be great, i almost finished migrating to version where each menu item is separate NavigationItem instance.
I just did a quick Menu system. Main things that do not work is.
<?php
namespace Fdw\Admin\Menu;
use Illuminate\Support\Facades\HTML;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\URL;
class Menu {
protected $items;
protected $current;
public function __construct() {
$this->current = Request::url();
}
public static function create($callback) {
$menu = new Menu();
$callback($menu);
return $menu;
}
public function add($key, $name, $url = null, $icon = null)
{
$item = array(
'key' => $key,
'name' => $name,
'url' => $url,
'icon' => $icon,
'children' => array()
);
$children = str_replace('.', '.children.', $key);
array_set($this->items, $children, $item);
}
public function render($items = null, $level = null)
{
$items = $items ?: $this->items;
$level = $level ?: 1;
$attr = array(
'class' => 1 === $level ? 'menu level-1' : 'level-'.$level
);
$menu = '<ul'.HTML::attributes($attr).'>';
foreach ($items as $key => $item) {
$classes = array('menu__item');
$classes[] = $this->getActive($item);
$has_children = sizeof($item['children']);
if ($has_children) {
$classes[] = 'parent';
}
$menu .= '<li'.HTML::attributes(array('class' => implode(' ', $classes))).'>';
$menu .= $this->createAnchor($item);
$menu .= ($has_children) ? $this->render($item['children'], ++$level) : '';
$menu .= '</li>';
}
$menu .= '</ul>';
return $menu;
}
private function createAnchor($item)
{
$output = '<a class="menu__link" href="'.$item['url'].'">';
$output .= $this->createIcon($item);
$output .= '<span class="menu__name">'.$item['name'].'</span>';
$output .= '</a>';
return $output;
}
private function createIcon($item)
{
$output = '';
if ($item['icon']) {
$output .= sprintf(
'<span class="menu__icon"><img src="%s" alt="%s"></span>',
$item['icon'],
$item['name']
);
}
return $output;
}
private function getActive($item)
{
$url = trim($item['url'], '/');
if ($this->current === $url)
{
return 'active current';
}
//TODO: Work out a way of adding active class on the parents.
}
}
- Sorting
- Traversing of the active class.
About second one: When adding item if it is active, set as active, than trim from end of full path ($key in add method) last item (to get parent item) and set for him something like hasActiveChild, and so on.
About first one: i am still thinking of best solution for sorting. If i have five items and they orders are: null, 1, null, 5, null. They should be sorted like this: 1, null, null, null, 5 or if i have 1, 3, null, 3, null, 5 it should be -> 1, null, 3, 3, 5, null.
So each number would be i its required place (or later if the same order passed multiple times) and nulls would take up empty places.
If you haven't done so already, check out PongoCMS. The new one this guy is releasing will have this functionality. https://github.com/PongoCMS/cms/tree/dev/src/Pongo/Cms
I'm also looking into my own implementation of this and think it would be cool if we found the best solution for this.
Ok I have actually finished it as a rough draft. The sorting works perfectly as long as the keys are in some form of heirachy.
After the menu items are created it will also run a sort on it based on the sort key.
If you have any idea's for improvements I would love to hear. This was still a pretty fast attempt.
<?php
namespace Fdw\Admin;
use Illuminate\Support\Facades\HTML;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\URL;
class Menu {
protected $items;
protected $current;
protected $currentKey;
public function __construct() {
$this->current = Request::url();
}
/**
* Shortcut method for create a menu with a callback.
* This will allow you to do things like fire an event on creation.
*
* @param callable $callback Callback to use after the menu creation
* @return object
*/
public static function create($callback) {
$menu = new Menu();
$callback($menu);
$menu->sortItems();
return $menu;
}
/**
* Add a menu item to the item stack
*
* @param string $key Dot seperated heirarchy
* @param string $name Text for the anchor
* @param string $url URL for the anchor
* @param integer $sort Sorting index for the items
* @param string $icon URL to use for the icon
*/
public function add($key, $name, $url, $sort = 0, $icon = null)
{
$item = array(
'key' => $key,
'name' => $name,
'url' => $url,
'sort' => $sort,
'icon' => $icon,
'children' => array()
);
$children = str_replace('.', '.children.', $key);
array_set($this->items, $children, $item);
if ($url == $this->current) {
$this->currentKey = $key;
}
}
/**
* Recursive function to loop through items and create a menu
*
* @param array $items List of items that need to be rendered
* @param boolean $level Which level you are currently rendering
* @return string
*/
public function render($items = null, $level = 1)
{
$items = $items ?: $this->items;
$attr = array(
'class' => 1 === $level ? 'menu level-1' : 'level-'.$level
);
$menu = '<ul'.HTML::attributes($attr).'>';
foreach ($items as $key => $item) {
$classes = array('menu__item');
$classes[] = $this->getActive($item);
$has_children = sizeof($item['children']);
if ($has_children) {
$classes[] = 'parent';
}
$menu .= '<li'.HTML::attributes(array('class' => implode(' ', $classes))).'>';
$menu .= $this->createAnchor($item);
$menu .= ($has_children) ? $this->render($item['children'], ++$level) : '';
$menu .= '</li>';
}
$menu .= '</ul>';
return $menu;
}
/**
* Method to render an anchor
*
* @param array $item Item that needs to be turned into a link
* @return string
*/
private function createAnchor($item)
{
$output = '<a class="menu__link" href="'.$item['url'].'">';
$output .= $this->createIcon($item);
$output .= '<span class="menu__name">'.$item['name'].'</span>';
$output .= '</a>';
return $output;
}
/**
* Method to render an icon
*
* @param array $item Item that needs to be turned into a icon
* @return string
*/
private function createIcon($item)
{
$output = '';
if ($item['icon']) {
$output .= sprintf(
'<span class="menu__icon"><img src="%s" alt="%s"></span>',
$item['icon'],
$item['name']
);
}
return $output;
}
/**
* Method to sort through the menu items and put them in order
*
* @return void
*/
private function sortItems() {
usort($this->items, function($a, $b) {
if ($a['sort'] == $b['sort']) {
return 0;
}
return ($a['sort'] < $b['sort']) ? -1 : 1;
});
}
/**
* Method to find the active links
*
* @param array $item Item that needs to be checked if active
* @return string
*/
private function getActive($item)
{
$url = trim($item['url'], '/');
if ($this->current === $url)
{
return 'active current';
}
if (strpos($this->currentKey, $item['key']) === 0) {
return 'active';
}
}
}
Sorry , to hijjack this subject but can I also work to make this sort of menu : http://laravel.io/forum/09-18-2014-how-can-i-make-this-menu-the-best
Roelof
Try my Laravel 5 Package for Creating Dynamic, Database Driven, Bootstrap supported, Drop Down Menu.
Sign in to participate in this thread!
The Laravel portal for problem solving, knowledge sharing and community building.
The community