From 8f76b0697c496376054a90258e2d6e9a69a48241 Mon Sep 17 00:00:00 2001 From: Jeroen Noten Date: Sat, 20 Aug 2016 20:07:55 +0200 Subject: [PATCH] Improve active menu item detection --- composer.json | 5 + phpunit.xml | 2 +- .../partials/menu-item-top-nav.blade.php | 12 +- resources/views/partials/menu-item.blade.php | 10 +- src/AdminLte.php | 12 +- src/Menu/ActiveChecker.php | 71 ++++++++ src/Menu/Builder.php | 81 ++++++++- tests/AdminLteTest.php | 19 ++ tests/Menu/ActiveCheckerTest.php | 95 ++++++++++ tests/Menu/BuilderTest.php | 166 ++++++++++++++++++ tests/ServiceProviderTest.php | 5 +- tests/TestCase.php | 60 +++++++ 12 files changed, 516 insertions(+), 22 deletions(-) create mode 100644 src/Menu/ActiveChecker.php create mode 100644 tests/AdminLteTest.php create mode 100644 tests/Menu/ActiveCheckerTest.php create mode 100644 tests/Menu/BuilderTest.php create mode 100644 tests/TestCase.php diff --git a/composer.json b/composer.json index f293113b..1ecda30d 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,11 @@ "JeroenNoten\\LaravelAdminLte\\": "src/" } }, + "autoload-dev": { + "classmap": [ + "tests/TestCase.php" + ] + }, "require": { "laravel/framework": "5.1.*|5.2.*|5.3.*", "php": ">=5.5.9" diff --git a/phpunit.xml b/phpunit.xml index b5415e32..b41bdaad 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,5 @@ - + ./tests diff --git a/resources/views/partials/menu-item-top-nav.blade.php b/resources/views/partials/menu-item-top-nav.blade.php index 93ff03b5..f3b8bb93 100644 --- a/resources/views/partials/menu-item-top-nav.blade.php +++ b/resources/views/partials/menu-item-top-nav.blade.php @@ -1,10 +1,6 @@ @if (is_array($item)) - @endif @else -
  • - +
  • + {{ $subitem['text'] }} @if (isset($subitem['label'])) diff --git a/resources/views/partials/menu-item.blade.php b/resources/views/partials/menu-item.blade.php index 42ac9cfc..f1ae0b8b 100644 --- a/resources/views/partials/menu-item.blade.php +++ b/resources/views/partials/menu-item.blade.php @@ -1,12 +1,8 @@ @if (is_string($item))
  • {{ $item }}
  • @else -
  • - +
  • + {{ $item['text'] }} @if (isset($item['label'])) @@ -18,7 +14,7 @@ @endif @if (isset($item['submenu'])) -
      +
        @each('adminlte::partials.menu-item', $item['submenu'], 'item')
      @endif diff --git a/src/AdminLte.php b/src/AdminLte.php index c9050f58..f5438ab1 100644 --- a/src/AdminLte.php +++ b/src/AdminLte.php @@ -5,7 +5,9 @@ use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Routing\UrlGenerator; use JeroenNoten\LaravelAdminLte\Events\BuildingMenu; +use JeroenNoten\LaravelAdminLte\Menu\ActiveChecker; use JeroenNoten\LaravelAdminLte\Menu\Builder; class AdminLte @@ -14,9 +16,15 @@ class AdminLte private $events; - public function __construct(Dispatcher $events) + private $urlGenerator; + + private $activeChecker; + + public function __construct(Dispatcher $events, UrlGenerator $urlGenerator, ActiveChecker $activeChecker) { $this->events = $events; + $this->urlGenerator = $urlGenerator; + $this->activeChecker = $activeChecker; } public function menu() @@ -30,7 +38,7 @@ public function menu() protected function buildMenu() { - $builder = new Builder; + $builder = new Builder($this->urlGenerator, $this->activeChecker); $this->events->fire(new BuildingMenu($builder)); diff --git a/src/Menu/ActiveChecker.php b/src/Menu/ActiveChecker.php new file mode 100644 index 00000000..c9fc1981 --- /dev/null +++ b/src/Menu/ActiveChecker.php @@ -0,0 +1,71 @@ +request = $request; + } + + public function isActive($item) + { + if (isset($item['active'])) { + return $this->isExplicitActive($item['active']); + } + + if (isset($item['submenu'])) { + return $this->containsActive($item['submenu']); + } + + if (isset($item['url'])) { + return $this->isActiveUrl($item['url']); + } + + return false; + } + + protected function isActiveUrl($url) + { + return $this->checkExact($url) || $this->checkSub($url); + } + + protected function checkExact($url) + { + return $this->request->is($url); + } + + protected function checkSub($url) + { + return $this->request->is($url . '/*'); + } + + protected function containsActive($items) + { + foreach ($items as $item) { + if ($this->isActive($item)) { + return true; + } + } + + return false; + } + + private function isExplicitActive($active) + { + foreach ($active as $url) { + if ($this->isActiveUrl($url)) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/Menu/Builder.php b/src/Menu/Builder.php index e8e7bbee..cab81f76 100644 --- a/src/Menu/Builder.php +++ b/src/Menu/Builder.php @@ -4,16 +4,93 @@ namespace JeroenNoten\LaravelAdminLte\Menu; +use Illuminate\Contracts\Routing\UrlGenerator; + class Builder { public $menu = []; + private $urlGenerator; + + private $activeChecker; + + public function __construct(UrlGenerator $urlGenerator, ActiveChecker $activeChecker) + { + $this->urlGenerator = $urlGenerator; + $this->activeChecker = $activeChecker; + } + public function add() { - $items = func_get_args(); + $items = $this->transformItems(func_get_args()); - foreach($items as $item) { + foreach ($items as $item) { array_push($this->menu, $item); } } + + protected function transformItems($items) + { + return array_map([$this, 'transformItem'], $items); + } + + protected function transformItem($item) + { + if (is_string($item)) { + return $item; + } + + $item['href'] = $this->makeHref($item); + $item['active'] = $this->isActive($item); + + if (isset($item['submenu'])) { + $item['submenu'] = $this->transformItems($item['submenu']); + $item['submenu_open'] = $item['active']; + $item['submenu_classes'] = $this->makeSubmenuClasses($item); + $item['submenu_class'] = implode(' ', $item['submenu_classes']); + } + + $item['classes'] = $this->makeClasses($item); + $item['class'] = implode(' ', $item['classes']); + $item['top_nav_classes'] = $this->makeClasses($item, true); + $item['top_nav_class'] = implode(' ', $item['top_nav_classes']); + + return $item; + } + + protected function makeHref($item) + { + if (!isset($item['url'])) { + return '#'; + } + + return $this->urlGenerator->to($item['url']); + } + + protected function makeClasses($item, $topNav = false) + { + $classes = []; + + if ($item['active']) { + $classes[] = 'active'; + } + + if (isset($item['submenu'])) { + $classes[] = $topNav ? 'dropdown': 'treeview'; + } + + return $classes; + } + + protected function isActive($item) + { + return $this->activeChecker->isActive($item); + } + + protected function makeSubmenuClasses($item) + { + $classes = ['treeview-menu']; + + return $classes; + } } \ No newline at end of file diff --git a/tests/AdminLteTest.php b/tests/AdminLteTest.php new file mode 100644 index 00000000..bb283335 --- /dev/null +++ b/tests/AdminLteTest.php @@ -0,0 +1,19 @@ +makeAdminLte(); + + $this->getDispatcher()->listen(BuildingMenu::class, function (BuildingMenu $event) { + $event->menu->add(['text' => 'Home']); + }); + + $menu = $adminLte->menu(); + + $this->assertEquals('Home', $menu[0]['text']); + } +} \ No newline at end of file diff --git a/tests/Menu/ActiveCheckerTest.php b/tests/Menu/ActiveCheckerTest.php new file mode 100644 index 00000000..27f4f1c9 --- /dev/null +++ b/tests/Menu/ActiveCheckerTest.php @@ -0,0 +1,95 @@ +makeActiveChecker('http://example.com/about'); + + $this->assertTrue($checker->isActive(['url' => 'about'])); + } + + public function testRoot() + { + $checker = $this->makeActiveChecker('http://example.com'); + + $this->assertTrue($checker->isActive(['url' => '/'])); + } + + public function testNotActive() + { + $checker = $this->makeActiveChecker('http://example.com/about'); + + $this->assertFalse($checker->isActive(['url' => 'home'])); + } + + public function testStringNotActive() + { + $checker = $this->makeActiveChecker(); + + $this->assertFalse($checker->isActive('HEADER')); + } + + public function testSub() + { + $checker = $this->makeActiveChecker('http://example.com/about/sub'); + + $this->assertTrue($checker->isActive(['url' => 'about'])); + } + + public function testSubmenu() + { + $checker = $this->makeActiveChecker('http://example.com/home'); + + $isActive = $checker->isActive([ + 'submenu' => [ + ['url' => 'home'] + ] + ]); + + $this->assertTrue($isActive); + } + + public function testMultiLevelSubmenu() + { + $checker = $this->makeActiveChecker('http://example.com/home'); + + $isActive = $checker->isActive([ + 'text' => 'Level 0', + 'submenu' => [ + [ + 'text' => 'Level 1', + 'submenu' => [ + ['url' => 'home'] + ] + ] + ] + ]); + + $this->assertTrue($isActive); + } + + public function testExplicitActive() + { + $checker = $this->makeActiveChecker('http://example.com/home'); + + $isActive = $checker->isActive([ + 'active' => ['home'] + ]); + + $this->assertTrue($isActive); + } + + public function testExplicitActiveRegex() + { + $checker = $this->makeActiveChecker('http://example.com/home/sub'); + + $isActive = $checker->isActive([ + 'active' => ['home/*'] + ]); + + $this->assertTrue($isActive); + } + +} \ No newline at end of file diff --git a/tests/Menu/BuilderTest.php b/tests/Menu/BuilderTest.php new file mode 100644 index 00000000..a81dfbd8 --- /dev/null +++ b/tests/Menu/BuilderTest.php @@ -0,0 +1,166 @@ +makeMenuBuilder(); + + $builder->add(['text' => 'Home', 'url' => '/']); + + $this->assertEquals('Home', $builder->menu[0]['text']); + $this->assertEquals('/', $builder->menu[0]['url']); + } + + public function testAddMultipleItems() + { + $builder = $this->makeMenuBuilder(); + + $builder->add(['text' => 'Home', 'url' => '/']); + $builder->add(['text' => 'About', 'url' => '/about']); + + $this->assertEquals('Home', $builder->menu[0]['text']); + $this->assertEquals('/', $builder->menu[0]['url']); + $this->assertEquals('About', $builder->menu[1]['text']); + $this->assertEquals('/about', $builder->menu[1]['url']); + } + + public function testAddMultipleItemsAtOnce() + { + $builder = $this->makeMenuBuilder(); + + $builder->add( + ['text' => 'Home', 'url' => '/'], + ['text' => 'About', 'url' => '/about'] + ); + + $this->assertEquals('Home', $builder->menu[0]['text']); + $this->assertEquals('/', $builder->menu[0]['url']); + $this->assertEquals('About', $builder->menu[1]['text']); + $this->assertEquals('/about', $builder->menu[1]['url']); + } + + public function testHrefWillBeAdded() + { + $builder = $this->makeMenuBuilder(); + + $builder->add(['text' => 'Home', 'url' => '/']); + $builder->add(['text' => 'About', 'url' => '/about']); + + $this->assertEquals('http://example.com', $builder->menu[0]['href']); + $this->assertEquals('http://example.com/about', $builder->menu[1]['href']); + } + + public function testDefaultHref() + { + $builder = $this->makeMenuBuilder(); + + $builder->add(['text' => 'Home']); + + $this->assertEquals('#', $builder->menu[0]['href']); + } + + public function testSubmenuHref() + { + $builder = $this->makeMenuBuilder(); + + $builder->add([ + 'text' => 'Home', + 'submenu' => [ + ['text' => 'About', 'url' => '/about'] + ] + ]); + + $this->assertEquals('http://example.com/about', $builder->menu[0]['submenu'][0]['href']); + } + + public function testMultiLevelSubmenuHref() + { + $builder = $this->makeMenuBuilder(); + + $builder->add([ + 'text' => 'Home', + 'submenu' => [ + [ + 'text' => 'About', + 'url' => '/about', + 'submenu' => [ + ['text' => 'Test', 'url' => '/test'] + ] + ] + ] + ]); + + $this->assertEquals('http://example.com/test', $builder->menu[0]['submenu'][0]['submenu'][0]['href']); + } + + public function testActiveClass() + { + $builder = $this->makeMenuBuilder('http://example.com/about'); + + $builder->add(['text' => 'About', 'url' => 'about']); + + $this->assertContains('active', $builder->menu[0]['classes']); + } + + public function testSubmenuActiveWithHash() + { + $builder = $this->makeMenuBuilder('http://example.com/home'); + + $builder->add([ + 'url' => '#', + 'submenu' => [ + ['url' => 'home'], + ], + ]); + + $this->assertTrue($builder->menu[0]['active']); + } + + public function testTreeviewClass() + { + $builder = $this->makeMenuBuilder(); + + $builder->add(['text' => 'About', 'submenu' => []]); + + $this->assertContains('treeview', $builder->menu[0]['classes']); + $this->assertContains('dropdown', $builder->menu[0]['top_nav_classes']); + } + + public function testTreeviewMenuSubmenuClasses() + { + $builder = $this->makeMenuBuilder(); + + $builder->add(['text' => 'About', 'submenu' => []]); + + $this->assertContains('treeview-menu', $builder->menu[0]['submenu_classes']); + } + + public function testSubmenuClass() + { + $builder = $this->makeMenuBuilder(); + + $builder->add(['text' => 'About', 'submenu' => []]); + + $this->assertEquals('treeview-menu', $builder->menu[0]['submenu_class']); + } + + public function testClass() + { + $builder = $this->makeMenuBuilder('http://example.com/about'); + + $builder->add(['text' => 'About', 'url' => 'about']); + + $this->assertEquals('active', $builder->menu[0]['class']); + } + + public function testTopNavClass() + { + $builder = $this->makeMenuBuilder('http://example.com/about'); + + $builder->add(['text' => 'About', 'url' => 'about']); + + $this->assertEquals('active', $builder->menu[0]['top_nav_class']); + } + +} \ No newline at end of file diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php index 5881ab13..8f2f2c7e 100644 --- a/tests/ServiceProviderTest.php +++ b/tests/ServiceProviderTest.php @@ -7,7 +7,7 @@ use JeroenNoten\LaravelAdminLte\Menu\Builder; use JeroenNoten\LaravelAdminLte\ServiceProvider; -class ServiceProviderTest extends PHPUnit_Framework_TestCase +class ServiceProviderTest extends TestCase { public function testRegisterMenu() { @@ -15,7 +15,8 @@ public function testRegisterMenu() $config = new Repository([ 'adminlte.menu' => ['item'] ]); - $menuBuilder = new Builder(); + + $menuBuilder = $this->makeMenuBuilder(); ServiceProvider::registerMenu($events, $config); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..97673bc2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,60 @@ +makeUrlGenerator($uri), + $this->makeActiveChecker($uri) + ); + } + + protected function makeActiveChecker($uri = 'http://example.com') + { + return new ActiveChecker($this->makeRequest($uri)); + } + + private function makeRequest($uri) + { + return Request::createFromBase(SymfonyRequest::create($uri)); + } + + protected function makeAdminLte() + { + return new AdminLte( + $this->getDispatcher(), + $this->makeUrlGenerator(), + $this->makeActiveChecker() + ); + } + + protected function makeUrlGenerator($uri = 'http://example.com') + { + return new UrlGenerator( + new RouteCollection, + $this->makeRequest($uri) + ); + } + + protected function getDispatcher() + { + if (!$this->dispatcher) { + $this->dispatcher = new Dispatcher; + } + + return $this->dispatcher; + } +} \ No newline at end of file