diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..2c1fc0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +composer.lock +.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100755 index 0000000..0edb59c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: php + +php: + - 5.3 + - 5.4 + +before_script: + - curl -s http://getcomposer.org/installer | php + - php composer.phar install --dev + +script: phpunit \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..514cad6 --- /dev/null +++ b/README.md @@ -0,0 +1,144 @@ +# Purpose Media Menu Builder for Laravel 4 + + +## Usage + + +Very simple method of building menus from database data (id, parent id), service provider register methods for application modules and much more. + + +## Example 1 + + +```php + 'Home', 'URL' => '/', 'reference' => '0' ) ); +Menu::render(); +?> +``` + +##Example 2 - Nesting Children + + +```php + 'Services', 'URL' => '/services/', 'reference' => '1', 'parent' => '0' ) ); +Menu::render(); +?> +``` + +## Example 3 - Multiple Menus + + +```php + 'Services', 'URL' => '/services/', 'reference' => '1', 'parent' => '0' ) )->toMenu( 'main' ); +Menu::render( 'main' ); +?> +``` + +## Auto classes + + +I have added in some of the most used and required classes for styling menus + + +```css +.first-item {} +.last-item {} +.current-root {} +.current-parent {} +.current-ancestor {} +.has-children {} +``` + +## Output + + +```php + 'Home', 'URL' => '/menu-test-2/public/', 'reference' => '1', 'class' => 'home-icon', 'weight' => 0 ) )->toMenu( 'main' ); +Menu::addItem( array( 'text' => 'Services', 'URL' => '/menu-test-2/public/services/', 'reference' => '2' ) )->toMenu( 'main' ); +Menu::addItem( array( 'text' => 'Development', 'URL' => '/menu-test-2/public/services/development/', 'reference' => '3', 'parent' => '2' ) )->toMenu( 'main' ); +Menu::addItem( array( 'text' => 'Design', 'URL' => '/menu-test-2/public/services/design/', 'reference' => '4', 'parent' => '2', 'weight' => 0 ) )->toMenu( 'main' ); +Menu::render( 'main' ); +?> +``` + +```html + +``` + +## Use with third party menu UI through L4 Model +(Please note this is just a general summary of how it would work if you had 2 tables (and models) for navigations and navigation items with a standard hasMany() relationship) + + +```php +where( 'navigation_slug', '=', 'main' )->get(); +foreach( $navigation->navigationItems as $item ) +{ + Menu::addItem( array( 'text' => , $item->name 'URL' => $item->url, 'reference' => $item->id, 'parent' => $item->parent_id, 'weight' => $item->order ) )->toMenu( $navigation->navigation_slug ); +} +Menu::render( $navigation->navigation_slug ); +?> +``` + +## Install + +Add the following to you applications composer.json file + + +```json +"require": { + ... + "purposemedia/menu" : "dev-master" +}, +``` + +Run the following from your terminal from your application route (make sure you have access to composer.phar) + + +```shell +php composer.phar update +``` + +add the following to your /app/config/app.php's provider array. + + +```php +'Purposemedia\Menu\MenuServiceProvider' +``` + + +add the following to your /app/config/app.php's aliases array. + + +```php +'Menu' => 'Purposemedia\Menu\Facades\Menu' +``` + + +and finally back to your terminal and run + + +```shell +php composer.phar dump-autoload +``` + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1f69ef9 --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "name": "purposemedia/menu", + "description": "Effortless menu building for Laravel 4", + "homepage": "https://bitbucket.org/purposemedia/menu/", + "license": "MIT", + "authors": [ + { + "name": "Luke Snowden", + "email": "luke@purposemedia.co.uk" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-0": { + "Purposemedia\\Menu": "src/" + } + }, + "scripts": { + "post-update-cmd": [ + "php artisan package:install purposemedia/menu" + ], + "post-create-project-cmd": [ + "php artisan key:generate", + "php artisan package:install purposemedia/menu" + ] + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e89ac6d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + \ No newline at end of file diff --git a/provides.json b/provides.json new file mode 100644 index 0000000..9089a2f --- /dev/null +++ b/provides.json @@ -0,0 +1,11 @@ +{ + "providers": [ + "Purposemedia\Menu\MenuServiceProvider" + ], + "aliases": [ + { + "alias": "Menu", + "facade": "Purposemedia\Menu\Facades\Menu" + } + ] +} \ No newline at end of file diff --git a/src/Purposemedia/Menu/Facades/Menu.php b/src/Purposemedia/Menu/Facades/Menu.php new file mode 100644 index 0000000..bb78ab4 --- /dev/null +++ b/src/Purposemedia/Menu/Facades/Menu.php @@ -0,0 +1,19 @@ + '!%3A!ui', + "/" => '!%2F!ui', + "?" => '!%3F!ui', + "#" => '!%23!ui', + "[" => '!%5B!ui', + "]" => '!%5D!ui', + "@" => '!%40!ui', + "!" => '!%21!ui', + "$" => '!%24!ui', + "&" => '!%26!ui', + "'" => '!%27!ui', + "(" => '!%28!ui', + ")" => '!%29!ui', + "*" => '!%2A!ui', + "+" => '!%2B!ui', + "," => '!%2C!ui', + ";" => '!%3B!ui', + "=" => '!%3D!ui', + "%" => '!%25!ui', + ); + + $url = rawurlencode($url); + $url = preg_replace(array_values($reserved), array_keys($reserved), $url); + return $url; + } + +} + +?> \ No newline at end of file diff --git a/src/Purposemedia/Menu/Menu.php b/src/Purposemedia/Menu/Menu.php new file mode 100644 index 0000000..665983c --- /dev/null +++ b/src/Purposemedia/Menu/Menu.php @@ -0,0 +1,43 @@ + \camel_case( $match[1] ) ) ); + } + return call_user_func_array( array( static::container(), $method ), $parameters ); + } + +} + diff --git a/src/Purposemedia/Menu/MenuContainer.php b/src/Purposemedia/Menu/MenuContainer.php new file mode 100644 index 0000000..77aa74f --- /dev/null +++ b/src/Purposemedia/Menu/MenuContainer.php @@ -0,0 +1,133 @@ + '', + 'URL' => '#', + 'reference' => 0, + 'parent' => false, + 'weight' => 1, + 'class' => '', + 'children' => array(), + 'icon' => '', + 'attributes' => array() + ); + $this->items[] = array_merge( $defaults, $perams ); + return $this; + } + + /* + * @method To Menu + * @author Luke Snowden + * @param $name (string) + */ + + public function toMenu( $name ) + { + $name = \camel_case( $name ); + if( ! isset( $this->navigations[$name] ) ) + { + $this->navigations[$name] = new MenuContainerNavigation( $name ); + } + $this->navigations[$name]->addItem( array_pop( $this->items ) ); + } + + /* + * @method Render + * @author Luke Snowden + * @param $name (false/string) + */ + + public function render( $name = false ) + { + if( isset( $this->renders[\camel_case($name)] ) ) + { + return $this->renders[\camel_case($name)]; + } + if( ! $name ) + { + $this->navigations['pmDefaultMenu'] = new MenuContainerNavigation( 'pmDefaultMenu' ); + while( count( $this->items ) !== 0 ) + { + $item = array_shift( $this->items ); + $this->navigations['pmDefaultMenu']->addItem( $item ); + } + $this->renders[$name] = ''; + foreach( $this->navigations as $navigation ) + { + $this->renders[$name] .= $navigation->render(); + } + return $this->renders[$name]; + } + else + { + $name = \camel_case( $name ); + if( ! isset( $this->navigations[$name] ) ) + { + // This gets annoying! + // Throw new \Exception( "Navigation '{$name}' does not exist. Cannot process render." ); + return false; + } + $this->renders[$name] = $this->navigations[$name]->render(); + return $this->renders[$name]; + } + } + + /* + * @method Set Menu type + * @author Luke Snowden + * @param $type (false/string) + * @param $menu (false/string) + */ + + public function setMenuType( $type = false, $menu = false, $location = false ) + { + if( ! isset( $this->navigations[$menu] ) ) + { + Throw new \Exception( "Menu '{$menu}' does not exist or you have called this method before the menu has been created." ); + } + if( $location === false ) + { + $location = $this->stylesLocation; + } + $this->navigations[$menu]->setType( $type, $location ); + } + + +} diff --git a/src/Purposemedia/Menu/MenuContainerNavigation.php b/src/Purposemedia/Menu/MenuContainerNavigation.php new file mode 100644 index 0000000..7e182af --- /dev/null +++ b/src/Purposemedia/Menu/MenuContainerNavigation.php @@ -0,0 +1,411 @@ +name = $name; + } + + /* + * @method Add Item + * @author Luke Snowden + * @param $text (string), $url (string), $reference (int), $parent (false/int) + */ + + public function addItem( $item = array() ) + { + $defaults = array( + 'reference' => 0, + 'text' => '', + 'URL' => '#', + 'parent' => false, + 'children' => array(), + 'class' => '', + 'weight' => 1, + 'icon' => '', + 'attributes' => array() + ); + $this->items[] = array_merge( $defaults, $item ); + } + + /* + * @method Render Details + * @author Luke Snowden + * @param $structure (array), $depth (int) + */ + + private static function renderAttributes( array $attributes ) + { + foreach( $attributes as $attribute => $value ) + { + echo "{$attribute}=\"{$value}\" "; + } + } + + private function renderDetail( $structure, $depth = 1 ) + { + if( $depth === 1 ) + { + ob_start(); + } + ?> + +
  • + + > + + + +
  • + + $elements ) + { + if( isset( $elements[$column] ) && is_array( $elements[$column] ) && ! empty( $elements[$column] ) ) + { + $array[$key][$column] = self::ausort( $elements[$column], $column ); + } + } + return $array; + } + + private function sortItems( $structure ) + { + $structure = self::ausort( $structure, 'weight' ); + foreach( $structure as $key => $item ) + { + $structure[$key]['class'] .= $key === 0 ? ' first-item' : ''; + $structure[$key]['class'] .= ! isset( $structure[$key+1] ) ? ' last-item' : ''; + if( ! empty( $item['children'] ) ) + { + $structure[$key]['children'] = $this->sortItems( $structure[$key]['children'] ); + } + } + return $structure; + } + + /* + * @method Render + * @author Luke Snowden + * @param (void) + */ + + public function render() + { + $structure = $this->generate(); + $structure = $this->sortItems( $structure ); + $return = ''; + + if( $this->type === 'default' ) + { + $return .= ""; + return $return; + } + else + { + $class = $this->stylesLocation . '\\Styles'; + if( ! class_exists( $class ) ) + { + Throw new \Exception( "{$class} does not exist" ); + } + $style = new $class(); + $method = \camel_case( "render-{$this->type}" ); + if( ! class_exists( $class, $method ) ) + { + Throw new \Exception( "{$method} does not exist" ); + } + $return .= ""; + return $return; + } + + } + + /* + * @method Current URI + * @author Luke Snowden + * @param (void) + */ + + public static function currentURI() + { + $fullLocation = rtrim( \URL::current(), '/' ) . '/'; + $domain = \Config::get( 'app.url' ); + return str_replace( '//', '/', '/' . trim( str_replace( $domain, '', $fullLocation ), '/' ) . '/' ); + } + + /* + * @method Get Roots + * @author Luke Snowden + * @param (void) + */ + + private function getRoots() + { + $x = 0; + $return = array(); + $count = count( $this->items ); + + while( $count >= $x ) + { + $item = array_shift($this->items); + if( $item['parent'] === false ) + { + $return[] = $item; + } + else + { + array_push( $this->items, $item ); + } + $x++; + } + return $return; + } + + /* + * @method Get Children + * @author Luke Snowden + * @param $ref (int) + */ + + private function getChildren( $ref ) + { + $x = 0; + $return = array(); + $count = count( $this->items ); + while( $count > $x ) + { + $item = array_shift( $this->items ); + if( (string)$item['parent'] == (string)$ref ) + { + $item['children'] = $this->getChildren( $item['reference'] ); + $return[] = $item; + } + else + { + array_push( $this->items, $item ); + } + $x++; + } + foreach( $return as $key => $item ) + { + $return[$key]['class'] .= count( $item['children'] ) > 0 ? ' has-children' : ''; + $return[$key]['class'] .= $this->isAnAncestor( $item['children'] ); + $return[$key]['class'] .= $this->isParentClass( $item ); + } + return $return; + } + + /* + * @method Is An Ancestor + * @author Luke Snowden + * @param $children (array) + */ + + private function isAnAncestor( $children ) + { + $currentURI = self::currentURI(); + foreach( $children as $child ) + { + if( $currentURI == self::cleanseToURI( $child['URL'] ) ) + { + return ' current-ancestor'; + } + if( ! empty( $child['children'] ) ) + { + if( ! is_null( $this->isAnAncestor( $child['children'] ) ) ) + { + return ' current-ancestor'; + } + } + } + return NULL; + } + + /* + * @method Is Parent Class + * @author Luke Snowden + * @param $item (array) + */ + + private function isParentClass( $item ) + { + $currentURI = self::currentURI(); + foreach( $item['children'] as $child ) + { + if( $currentURI == self::cleanseToURI( $child['URL'] ) ) + { + return ' current-parent'; + } + } + return ''; + } + + /* + * @method Sort By Weight + * @author Luke Snowden + * @param $a (array) + * @param $b (array) + */ + + private static function sortByWeight( $a, $b ) + { + return $a['weight'] - $b['weight']; + } + + /* + * @method Root class + * @author Luke Snowden + * @param $children (array) + */ + + private function rootClass( $children ) + { + $currentURI = self::currentURI(); + foreach( $children as $child ) + { + if( $currentURI == self::cleanseToURI( $child['URL'] ) ) + { + return ' current-root'; + } + if( ! empty( $child['children'] ) ) + { + if( ! is_null( $class = $this->rootClass( $child['children'] ) ) ) + { + return $class; + } + } + } + return NULL; + } + + /* + * @method Cleanse To URI + * @author Luke Snowden + * @param $url (string) + */ + + public static function cleanseToURI( $url ) + { + $domain = \Config::get( 'app.url' ); + if( preg_match( "#^https?://.*#", $url ) ) + { + return $url; + } + else + { + return str_replace( '//', '/', '/' . trim( str_replace( \Config::get( 'app.url' ), '', UTA::urlToAbsolute( \URL::current(), $url ) ), '/' ) . '/' ); + } + } + + /* + * @method Set Current Class + * @author Luke Snowden + * @param (void) + */ + + private function setCurrentClass() + { + $currentURI = self::currentURI(); + foreach( $this->items as $key => $item ) + { + if( $currentURI == self::cleanseToURI( $item['URL'] ) ) + { + $this->items[$key]['class'] .= ' current'; + } + } + } + + /* + * @method Generate + * @author Luke Snowden + * @param (void) + */ + + private function generate() + { + $this->setCurrentClass(); + $roots = $this->getRoots(); + foreach( $roots as $key => $item ) + { + $roots[$key]['children'] = $this->getChildren( $item['reference'] ); + $roots[$key]['class'] .= count( $roots[$key]['children'] ) > 0 ? ' has-children' : ''; + $roots[$key]['class'] .= $this->rootClass( $roots[$key]['children'] ); + } + return $roots; + } + + /* + * @method Set Type + * @author Luke Snowden + * @param $type (string) + * @param $stylesLocation (string) + */ + + public function setType( $type, $stylesLocation ) + { + $this->type = $type; + $this->stylesLocation = $stylesLocation; + } + +} \ No newline at end of file diff --git a/src/Purposemedia/Menu/MenuServiceProvider.php b/src/Purposemedia/Menu/MenuServiceProvider.php new file mode 100644 index 0000000..bb9a387 --- /dev/null +++ b/src/Purposemedia/Menu/MenuServiceProvider.php @@ -0,0 +1,42 @@ +app['menu'] = $this->app->share( function( $app ) + { + return new Menu; + }); + $this->app['config']->package( "purposemedia/menu", dirname( __FILE__ ) . "/../../../config" ); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + + public function provides() + { + return array(); + } + +} \ No newline at end of file diff --git a/src/Purposemedia/Menu/Styles/Styles.php b/src/Purposemedia/Menu/Styles/Styles.php new file mode 100644 index 0000000..4c5d4da --- /dev/null +++ b/src/Purposemedia/Menu/Styles/Styles.php @@ -0,0 +1,25 @@ + + +
  • + + + + +
  • + + \ No newline at end of file diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29