├── .gitignore ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── css └── ui-navbar.css ├── demo ├── app.js ├── favicon.ico ├── index.html ├── package.json ├── partials │ ├── home.html │ ├── metal-gear.html │ ├── state1.html │ ├── state2.html │ ├── state3.html │ └── state4.html └── server.js ├── e2e ├── navbar.po.js └── navbar.spec.js ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── release ├── css │ ├── ui-navbar.css │ └── ui-navbar.min.css └── js │ ├── ui-navbar.js │ └── ui-navbar.min.js ├── src └── navbar.js ├── template ├── navbar-li.html ├── navbar-li.html.js ├── navbar-tree-li.html ├── navbar-tree-li.html.js ├── navbar-ul.html └── navbar-ul.html.js └── test └── ui-navbar.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | 4 | node_modules 5 | 6 | npm-debug.log -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | test/fixtures/dontlint.txt 2 | node_modules/** -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "immed": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "sub": true, 7 | "boss": true, 8 | "eqnull": true, 9 | "quotmark": "single", 10 | "trailing": true, 11 | "undef": true, 12 | "browser": true, 13 | "jquery": true, 14 | "globals": { 15 | "angular": false, 16 | 17 | // For Grunt 18 | "require": false, 19 | 20 | // For Jasmine 21 | "after" : false, 22 | "afterEach" : false, 23 | "before" : false, 24 | "beforeEach" : false, 25 | "describe" : false, 26 | "expect" : false, 27 | "jasmine" : false, 28 | "module" : false, 29 | "spyOn" : false, 30 | "inject" : false, 31 | "it" : false 32 | } 33 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | 5 | before_script: 6 | - export CHROME_BIN=chromium-browser 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | - npm install --quiet -g karma 10 | 11 | script: grunt -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | // Load grunt tasks automatically 4 | require('load-grunt-tasks')(grunt); 5 | 6 | // Time how long tasks take. Can help when optimizing build times 7 | require('time-grunt')(grunt); 8 | 9 | grunt.initConfig({ 10 | pkg: grunt.file.readJSON('package.json'), 11 | 12 | release: 'release', 13 | filename: 'ui-navbar', 14 | meta: {}, 15 | 16 | connect: { 17 | server: { 18 | options: { 19 | port: 5000, 20 | base: 'src' 21 | } 22 | } 23 | }, 24 | 25 | karma: { 26 | unit: { 27 | configFile: 'karma.conf.js' 28 | }, 29 | travis: { 30 | configFile: 'karma.conf.js', 31 | singleRun: true, 32 | autoWatch: false 33 | } 34 | }, 35 | 36 | protractor: { 37 | e2e: { 38 | options: { 39 | args: { 40 | specs: ['e2e/**/*.spec.js'] 41 | }, 42 | configFile: 'protractor.conf.js', 43 | keepAlive: true 44 | } 45 | } 46 | }, 47 | 48 | protractor_webdriver: { 49 | start: { 50 | options: { 51 | path: './node_modules/grunt-protractor-runner/node_modules/protractor/bin/', 52 | command: 'webdriver-manager start' 53 | } 54 | } 55 | }, 56 | 57 | concat: { 58 | release: { 59 | src: [ 60 | 'src/**/*.js', 61 | 'template/**/*.js', 62 | '!**/*.min.js', 63 | '!**/*.spec.js' 64 | ], 65 | dest: 'release/js/<%= filename %>.js' 66 | } 67 | }, 68 | 69 | uglify: { 70 | release: { 71 | src: ['release/js/<%= filename %>.js'], 72 | dest: 'release/js/<%= filename %>.min.js' 73 | } 74 | }, 75 | 76 | cssmin: { 77 | build: { 78 | src: 'css/<%= filename %>.css', 79 | dest: 'release/css/<%= filename %>.min.css' 80 | } 81 | }, 82 | 83 | copy: { 84 | main: { 85 | files: [ 86 | {expand: true, src: ['css/ui-navbar.css'], dest: 'release/', filter: 'isFile'} 87 | ] 88 | } 89 | }, 90 | 91 | jshint: { 92 | files: ['Gruntfile.js', 'src/**/*.js'], 93 | options: { 94 | jshintrc: '.jshintrc' 95 | } 96 | }, 97 | 98 | html2js: { 99 | release: { 100 | options: { 101 | module: null, // no bundle for all the templates 102 | base: '.' 103 | }, 104 | files: [{ 105 | expand: true, 106 | src: ['template/**/*.html'], 107 | ext: '.html.js' 108 | }] 109 | } 110 | } 111 | }); 112 | 113 | grunt.registerTask('default', [ 114 | 'jshint', 115 | 'html2js', 116 | 'karma:travis', 117 | 'concat', 118 | 'uglify', 119 | 'cssmin', 120 | 'copy' 121 | ]); 122 | 123 | grunt.registerTask('build', [ 124 | 'jshint', 125 | 'html2js', 126 | 'karma:unit', 127 | 'concat', 128 | 'uglify', 129 | 'cssmin', 130 | 'copy' 131 | ]); 132 | 133 | grunt.registerTask('e2e', [ 134 | 'connect:server', 135 | 'protractor_webdriver:start', 136 | 'protractor:e2e' 137 | ]); 138 | 139 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Eugenio Lentini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui-navbar - Responsive navigation bar with submenu in AngularJS 2 | 3 | ![Bower](https://img.shields.io/bower/v/ui-navbar.svg) 4 | [![NPM](https://img.shields.io/npm/v/ui-navbar.svg)](https://www.npmjs.com/package/ui-navbar) 5 | [![Build Status](https://travis-ci.org/blackat/ui-navbar.svg?branch=master)](https://travis-ci.org/blackat/ui-navbar) 6 | [![Dependency Status](https://david-dm.org/blackat/ui-navbar/status.svg?branch=master)](https://david-dm.org/blackat/ui-navbar#info=dependencies) 7 | [![devDependency Status](https://david-dm.org/blackat/ui-navbar/dev-status.svg?branch=master)](https://david-dm.org/blackat/ui-navbar#info=devDependencies) 8 | 9 | ## Introduction 10 | Build a responsive navigation menu bar with sub-menu in a __recursive__ fashion using `ui-router` to load partials. 11 | 12 | The directive can now be used in 3 different ways: buttons or icons, navbar with separated drop-down menu or single tree structure. 13 | 14 | ## Plunkr live demo 15 | 16 | - version < 0.14.x Live demo at [Plunkr](http://plnkr.co/edit/V7tecYv4wNPP198HRQlJ?p=info) 17 | - version > 2.2.0 Live demo at [Plunkr](https://plnkr.co/edit/svsAXSVyeiJm8StMB07n) 18 | 19 | ## 1. Installation 20 | Via npm 21 | ``` 22 | npm install ui-navbar --save 23 | ``` 24 | 25 | or via Bower 26 | ``` 27 | bower install ui-navbar --save 28 | ``` 29 | 30 | ## 2. Configure routing in your module adding required dependencies 31 | ```javascript 32 | angular.module('App', ['ui.bootstrap', 'ui.router', 'ui.navbar']) 33 | 34 | .config(function ($stateProvider, $urlRouterProvider) { 35 | 36 | // For any unmatched url, redirect to /state1 37 | $urlRouterProvider.otherwise("/home"); 38 | 39 | // Now set up the states 40 | $stateProvider 41 | .state('home', { 42 | url: "/home", 43 | templateUrl: "home.html" 44 | }) 45 | .state('metal-gear', { 46 | url: "/metal-gear", 47 | templateUrl: "metal-gear.html" 48 | }) 49 | .state('metal-gear2', { 50 | url: "/metal-gear2", 51 | templateUrl: "metal-gear2.html" 52 | }) 53 | .state('metal-gear-solid', { 54 | url: "/metal-gear-solid", 55 | templateUrl: "metal-gear-solid.html" 56 | }); 57 | }); 58 | ``` 59 | 60 | ## 3. Configure the controller 61 | ```javascript 62 | angular.module('App').controller('NavigationController', function ($scope) { 63 | 64 | $scope.konami = [{ 65 | name: "Konami", 66 | link: "#", 67 | subtree: [{ 68 | name: "Metal Gear", 69 | link: "#", 70 | subtree: [{ 71 | name: "Metal Gear", 72 | link: "metal-gear" 73 | }, { 74 | name: "Metal Gear 2: Solid Snake", 75 | link: "metal-gear2" 76 | }, { 77 | name: "Metal Gear Solid: The Twin Snakes", 78 | link: "metal-gear-solid" 79 | }] 80 | }] 81 | }]; 82 | 83 | $scope.trees = [{ 84 | name: "Konami", 85 | link: "#", 86 | subtree: [{ 87 | name: "Metal Gear", 88 | link: "#", 89 | subtree: [{ 90 | name: "Metal Gear", 91 | link: "metal-gear" 92 | }, { 93 | name: "Metal Gear 2: Solid Snake", 94 | link: "#" 95 | }, { 96 | name: "Metal Gear Solid: The Twin Snakes", 97 | link: "#" 98 | }] 99 | }, { 100 | name: "divider", 101 | link: "#" 102 | }, { 103 | name: "Castlevania", 104 | link: "#", 105 | subtree: [{ 106 | ... 107 | }] 108 | }] 109 | }] 110 | } 111 | ``` 112 | 113 | ## 4. Html parts 114 | 115 | ### Add `ui-view` to attach the partials. 116 | ```html 117 | 118 |
119 | ``` 120 | 121 | 122 | ### Button 123 | Add a multi-level menu to a drop down button rendering the previously introduced items: 124 | ```javascript 125 |
126 | 129 | 130 |
131 | ``` 132 | 133 | ### Navigation bar with separated multi-level dropdown menu. 134 | Specify an array of states for each menu item in the navigation bar: 135 | ```html 136 |
137 | 181 |
182 | ``` 183 | 184 | 185 | ### Navigation bar with a single tree structure 186 | Specify an array representing the all tree, with all the states and subtree for of each 187 | state if required. 188 | ```html 189 |
190 | 209 |
210 | ``` 211 | 212 | ## Features 213 | 214 | - Recursive item menu definition in `json` format. 215 | - Easy way to define a `divider` between items. 216 | - Unlimited level of nesting. 217 | - Responsive. 218 | - Fully compatible with AngularJS. 219 | - Standard Html5 with AngularJS Bootstrap attributes such as `dropdown`. 220 | - Support tag `navbar-right` from Bootstrap with submenu opening on the left. 221 | - __No jquery required__ to manage responsivness and dropdown actions. 222 | 223 | ## Dependencies 224 | 225 | - AngularJS, required 1.5.x, tested with 1.5.8. 226 | - UI Boostrap, required 1.1.1, tested with 2.2.0. 227 | - ui-router, required 0.2.15, tested with 0.3.1. 228 | - Twitter Bootstrap, required 3.3.6, tested with 3.3.7. 229 | 230 | ## Update 231 | - Introduced the directive `` to specify the navigation bar in a _all-in-one_ fashion. 232 | - Updated the documentation, demo and plunker. 233 | 234 | ## Prefix 235 | - Prefixed `angular-ui-bootstrap` components in the `index.html` demo page according to the [migration guide](https://github.com/angular-ui/bootstrap/wiki/Migration-guide-for-prefixes). 236 | 237 | ## Demo 238 | From the folder `demo` type 239 | 240 | npm install 241 | node server.js 242 | 243 | then type in a browser `http://localhost:5000` to get the demo page working. 244 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-navbar", 3 | "version": "0.5.2", 4 | "homepage": "git@github.com:blackat/ui-navbar.git", 5 | "authors": [ 6 | "Eugenio Lentini (http://blackat.github.io)" 7 | ], 8 | "description": "Angular responsive navigation with recursive menu and sub-menu construction", 9 | "main": [ 10 | "release/js/ui-navbar.js", 11 | "release/css/ui-navbar.css" 12 | ], 13 | "keywords": [ 14 | "ui-boostrap", 15 | "boostrap", 16 | "ui-router", 17 | "navbar", 18 | "submenu", 19 | "menu", 20 | "navigation", 21 | "angularjs", 22 | "responsive" 23 | ], 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests" 30 | ], 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /css/ui-navbar.css: -------------------------------------------------------------------------------- 1 | .dropdown-submenu { 2 | position: relative; 3 | } 4 | 5 | .dropdown-submenu > .dropdown-menu { 6 | top: 0; 7 | left: 100%; 8 | margin-top: -6px; 9 | margin-left: 0px; 10 | -webkit-border-radius: 0; 11 | -moz-border-radius: 0; 12 | border-radius: 0; 13 | } 14 | 15 | .dropdown-submenu:hover > .dropdown-menu { 16 | display: block; 17 | } 18 | 19 | .dropdown-submenu > a:after { 20 | display: block; 21 | content: " "; 22 | float: right; 23 | width: 0; 24 | height: 0; 25 | border-color: transparent; 26 | border-style: solid; 27 | border-width: 5px 0 5px 5px; 28 | border-left-color: #ccc; 29 | margin-top: 5px; 30 | margin-right: -10px; 31 | } 32 | 33 | .dropdown-submenu:hover > a:after { 34 | border-left-color: #fff; 35 | } 36 | 37 | .dropdown-submenu.pull-left { 38 | float: none; 39 | } 40 | 41 | .dropdown-submenu.pull-left > .dropdown-menu { 42 | left: -100%; 43 | margin-left: 10px; 44 | -webkit-border-radius: 6px 0 6px 6px; 45 | -moz-border-radius: 6px 0 6px 6px; 46 | border-radius: 6px 0 6px 6px; 47 | } 48 | 49 | .dropdown-submenu-right { 50 | position: relative; 51 | } 52 | 53 | .dropdown-submenu-right > .dropdown-menu { 54 | top: 0; 55 | right: 100%; 56 | margin-top: -6px; 57 | margin-left: -1px; 58 | -webkit-border-radius: 0; 59 | -moz-border-radius: 0; 60 | border-radius: 0; 61 | } 62 | 63 | .dropdown-submenu-right:hover > .dropdown-menu { 64 | display: block; 65 | } 66 | 67 | .dropdown-submenu-right > a:after { 68 | display: block; 69 | content: " "; 70 | float: right; 71 | width: 0; 72 | height: 0; 73 | border-color: transparent; 74 | border-style: solid; 75 | border-width: 5px 0 5px 5px; 76 | border-left-color: #ccc; 77 | margin-top: 5px; 78 | margin-right: -10px; 79 | } 80 | 81 | .dropdown-submenu-right:hover > a:after { 82 | border-left-color: #fff; 83 | } 84 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('App', ['ui.bootstrap', 'ui.router', 'ui.navbar']) 4 | 5 | .config(function ($stateProvider, $urlRouterProvider) { 6 | 7 | // For any unmatched url, redirect to /state1 8 | $urlRouterProvider.otherwise("/home"); 9 | 10 | // Now set up the states 11 | $stateProvider 12 | .state('home', { 13 | url: "/home", 14 | templateUrl: "partials/home.html" 15 | }) 16 | .state('state1', { 17 | url: "/state1", 18 | templateUrl: "partials/state1.html" 19 | }) 20 | .state('state2', { 21 | url: "/state2", 22 | templateUrl: "partials/state2.html" 23 | }) 24 | .state('state3', { 25 | url: "/state3", 26 | templateUrl: "partials/state3.html" 27 | }) 28 | .state('state4', { 29 | url: "/state4", 30 | templateUrl: "partials/state4.html" 31 | }) 32 | .state('metal-gear', { 33 | url: "/metal-gear", 34 | templateUrl: "partials/metal-gear.html" 35 | }); 36 | }) 37 | 38 | .controller('NavigationController', function ($scope) { 39 | 40 | $scope.tree = [{ 41 | name: "States", 42 | link: "#", 43 | subtree: [{ 44 | name: "state 1", 45 | link: "state1", 46 | subtree: [{ 47 | name: "state 3", 48 | link: "state3" 49 | }] 50 | }, { 51 | name: "state 2", 52 | link: "state2", 53 | subtree: [{ 54 | name: "state 4", 55 | link: "state4" 56 | }] 57 | }] 58 | }, { 59 | name: "No states", 60 | link: "#", 61 | subtree: [{ 62 | name: "no state connected", 63 | link: "#" 64 | }] 65 | }, { 66 | name: "divider", 67 | link: "#" 68 | 69 | }, { 70 | name: "State has not been set up", 71 | link: "#" 72 | }, { 73 | name: "divider", 74 | link: "#" 75 | }, { 76 | name: "Here again no state set up", 77 | link: "#" 78 | }]; 79 | 80 | $scope.trees = [{ 81 | name: "Konami", 82 | link: "#", 83 | subtree: [{ 84 | name: "Metal Gear", 85 | link: "#", 86 | subtree: [{ 87 | name: "Metal Gear", 88 | link: "metal-gear" 89 | }, { 90 | name: "Metal Gear 2: Solid Snake", 91 | link: "#" 92 | }, { 93 | name: "Metal Gear Solid: The Twin Snakes", 94 | link: "#" 95 | }] 96 | }, { 97 | name: "divider", 98 | link: "#" 99 | }, { 100 | name: "Castlevania", 101 | link: "#", 102 | subtree: [{ 103 | name: "Castlevania", 104 | link: "#" 105 | }, { 106 | name: "Castlevania II: Simon's Quest", 107 | link: "#" 108 | }, { 109 | name: "Castlevania III: Dracula's Curse", 110 | link: "#" 111 | }] 112 | }] 113 | }, { 114 | name: "SNK", 115 | link: "#", 116 | subtree: [{ 117 | name: "Fatal Fury", 118 | link: "#", 119 | subtree: [{ 120 | name: "Fatal Fury", 121 | link: "#" 122 | }, { 123 | name: "Fatal Fury 2", 124 | link: "#" 125 | }, { 126 | name: "Fatal Fury: King of Fighters", 127 | link: "#" 128 | }, { 129 | name: "Fatal Fury Special", 130 | link: "#" 131 | }] 132 | }, { 133 | name: "divider", 134 | link: "#" 135 | }, { 136 | name: "Metal Slug", 137 | link: "#", 138 | subtree: [{ 139 | name: "Metal Slug", 140 | link: "#" 141 | }, { 142 | name: "Metal Slug 2", 143 | link: "#" 144 | }, { 145 | name: "Metal Slug 3", 146 | link: "#" 147 | }, { 148 | name: "Metal Slug 4", 149 | link: "#" 150 | }, { 151 | name: "Metal Slug 5", 152 | link: "#" 153 | }, { 154 | name: "Metal Slug 6", 155 | link: "#" 156 | }, { 157 | name: "Metal Slug 7", 158 | link: "#" 159 | }, { 160 | name: "Metal Slug X", 161 | link: "#" 162 | }] 163 | }] 164 | }, { 165 | name: "Sega", 166 | link: "#" 167 | },{ 168 | name: "Nintendo", 169 | link: "#" 170 | }] 171 | }); -------------------------------------------------------------------------------- /demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackat/ui-navbar/7bef0bd34bca5557207e2ae604c5778abf9ec8af/demo/favicon.ico -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Navbar Demo 7 | 8 | 9 | 10 | 11 | 12 |
13 | 63 | 64 |
65 | 86 |
87 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-navbar-demo", 3 | "version": "0.4.3", 4 | "author": "Eugenio Lentini (http://blackat.github.io)", 5 | "description": "demo app for ui-navbar", 6 | "dependencies": { 7 | "angular-ui-bootstrap": "^2.2.0", 8 | "angular": "^1.5.8", 9 | "angular-ui-router": "^0.3.1", 10 | "bootstrap": "^3.3.7" 11 | }, 12 | "devDependencies": { 13 | "connect": "^3.1.x", 14 | "serve-static": "^1.4.x" 15 | }, 16 | "scripts": { 17 | "start": "node server.js" 18 | }, 19 | "license": "MIT" 20 | } -------------------------------------------------------------------------------- /demo/partials/home.html: -------------------------------------------------------------------------------- 1 | 2 |

Demo

3 |
4 |

How to use ui-navbar to load different partials based on routing

5 | 6 |

just play with the foo menu

-------------------------------------------------------------------------------- /demo/partials/metal-gear.html: -------------------------------------------------------------------------------- 1 | 2 |

Metal Gear

3 |
4 |

this is just a foo state called Metal Gear

-------------------------------------------------------------------------------- /demo/partials/state1.html: -------------------------------------------------------------------------------- 1 | 2 |

State 1

3 |
4 |

this is just a foo state called 1

-------------------------------------------------------------------------------- /demo/partials/state2.html: -------------------------------------------------------------------------------- 1 | 2 |

State 2

3 |
4 |

this is just a foo state called 2

-------------------------------------------------------------------------------- /demo/partials/state3.html: -------------------------------------------------------------------------------- 1 | 2 |

State 3

3 |
4 |

this is just a foo state called 3

-------------------------------------------------------------------------------- /demo/partials/state4.html: -------------------------------------------------------------------------------- 1 | 2 |

State 4

3 |
4 |

this is just a foo state called 4

-------------------------------------------------------------------------------- /demo/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var connect = require('connect'); 4 | var serveStatic = require('serve-static'); 5 | var app = connect(); 6 | 7 | app.use(serveStatic('./')); 8 | app.use(serveStatic('../')); 9 | app.listen(5000); 10 | console.log('Connect to Node.js server typing in browser address bar http://localhost:5000'); -------------------------------------------------------------------------------- /e2e/navbar.po.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by eugenio on 26/07/15. 3 | */ 4 | -------------------------------------------------------------------------------- /e2e/navbar.spec.js: -------------------------------------------------------------------------------- 1 | // Define a test suire 2 | describe('navigation bar e2e test suite', function () { 3 | 4 | beforeEach(function () { 5 | browser.get('http://0.0.0.0:8000/'); 6 | }); 7 | 8 | }); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Sep 26 2015 21:17:15 GMT+0200 (CEST) 3 | 4 | module.exports = function (config) { 5 | 6 | var configuration = { 7 | 8 | // base path that will be used to resolve all patterns (eg. files, exclude) 9 | basePath: '', 10 | 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['jasmine'], 15 | 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 20 | // include dependencies 21 | 'node_modules/angular/angular.js', 22 | 'node_modules/angular-mocks/angular-mocks.js', 23 | 'node_modules/angular-ui-router/release/angular-ui-router.js', 24 | 'node_modules/angular-ui-bootstrap/dist/ui-bootstrap.js', 25 | 26 | // include navbar directive 27 | 'src/navbar.js', 28 | 29 | // include directive templates 30 | 'template/navbar-ul.html.js', 31 | 'template/navbar-li.html.js', 32 | 'template/navbar-tree-li.html.js', 33 | 34 | // include test folder 35 | 'test/*.spec.js' 36 | ], 37 | 38 | 39 | // list of files to exclude 40 | exclude: [], 41 | 42 | 43 | // preprocess matching files before serving them to the browser 44 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 45 | preprocessors: {}, 46 | 47 | 48 | // test results reporter to use 49 | // possible values: 'dots', 'progress' 50 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 51 | reporters: ['progress'], 52 | 53 | 54 | // web server port 55 | port: 9876, 56 | 57 | 58 | // enable / disable colors in the output (reporters and logs) 59 | colors: true, 60 | 61 | 62 | // level of logging 63 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 64 | logLevel: config.LOG_INFO, 65 | 66 | 67 | // enable / disable watching file and executing tests whenever any file changes 68 | autoWatch: true, 69 | 70 | 71 | // start these browsers 72 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 73 | browsers: ['Chrome'], 74 | 75 | 76 | // Continuous Integration mode 77 | // if true, Karma captures browsers, runs the tests and exits 78 | singleRun: false, 79 | 80 | // Travis configuration to run Chrome 81 | customLaunchers: { 82 | Chrome_travis_ci: { 83 | base: 'Chrome', 84 | flags: ['--no-sandbox'] 85 | } 86 | } 87 | }; 88 | 89 | if (process.env.TRAVIS) { 90 | configuration.browsers = ['Chrome_travis_ci']; 91 | } 92 | 93 | config.set(configuration); 94 | }; 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-navbar", 3 | "version": "0.5.1", 4 | "author": "Eugenio Lentini (http://blackat.github.io)", 5 | "description": "Angular responsive navigation with recursive menu and sub-menu construction", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:blackat/ui-navbar.git" 9 | }, 10 | "dependencies": { 11 | "angular": "1.5.8", 12 | "angular-ui-bootstrap": "^2.2.0", 13 | "angular-ui-router": "^0.3.1", 14 | "bootstrap": "3.3.7" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": "1.5.8", 18 | "grunt": "^0.4.5", 19 | "grunt-cli": "^1.2.0", 20 | "grunt-contrib-concat": "~0.5.1", 21 | "grunt-contrib-connect": "^0.11.2", 22 | "grunt-contrib-copy": "~0.8.0", 23 | "grunt-contrib-cssmin": "*", 24 | "grunt-contrib-jshint": "~0.11.1", 25 | "grunt-contrib-nodeunit": "~0.4.1", 26 | "grunt-contrib-uglify": "~0.11.0", 27 | "grunt-html2js": "~0.3.2", 28 | "grunt-karma": "^0.12.0", 29 | "grunt-protractor-runner": "^2.0.0", 30 | "grunt-protractor-webdriver": "^0.2.0", 31 | "jasmine-core": "^2.3.4", 32 | "karma": "^0.13.3", 33 | "karma-chrome-launcher": "^0.2.0", 34 | "karma-jasmine": "^0.3.6", 35 | "load-grunt-tasks": "~3.3.0", 36 | "protractor": "3.0.0", 37 | "time-grunt": "~1.2.0" 38 | }, 39 | "main": "./release/js/ui-navbar.js", 40 | "keywords": [ 41 | "ui-boostrap", 42 | "boostrap", 43 | "ui-router", 44 | "navbar", 45 | "submenu", 46 | "menu", 47 | "navigation", 48 | "angularjs", 49 | "responsive" 50 | ], 51 | "license": "MIT", 52 | "scripts": { 53 | "patch": "npm version patch && npm publish && git push --tags", 54 | "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | seleniumAddress: 'http://localhost:4444/wd/hub', 3 | 4 | specs: [ 5 | 'e2e/**/*.spec.js' 6 | ], 7 | 8 | framework: 'jasmine', 9 | 10 | capabilities: { 11 | 'browserName': 'chrome' 12 | }, 13 | 14 | jasmineNodeOpts: { 15 | showColors: true, // use colors in the command line report 16 | defaultTimeoutInterval: 30000 17 | }, 18 | 19 | seleniumServerJar: './node_modules/grunt-protractor-runner/node_modules/protractor/selenium/selenium-server-standalone-2.45.0.jar' 20 | }; -------------------------------------------------------------------------------- /release/css/ui-navbar.css: -------------------------------------------------------------------------------- 1 | .dropdown-submenu { 2 | position: relative; 3 | } 4 | 5 | .dropdown-submenu > .dropdown-menu { 6 | top: 0; 7 | left: 100%; 8 | margin-top: -6px; 9 | margin-left: 0px; 10 | -webkit-border-radius: 0; 11 | -moz-border-radius: 0; 12 | border-radius: 0; 13 | } 14 | 15 | .dropdown-submenu:hover > .dropdown-menu { 16 | display: block; 17 | } 18 | 19 | .dropdown-submenu > a:after { 20 | display: block; 21 | content: " "; 22 | float: right; 23 | width: 0; 24 | height: 0; 25 | border-color: transparent; 26 | border-style: solid; 27 | border-width: 5px 0 5px 5px; 28 | border-left-color: #ccc; 29 | margin-top: 5px; 30 | margin-right: -10px; 31 | } 32 | 33 | .dropdown-submenu:hover > a:after { 34 | border-left-color: #fff; 35 | } 36 | 37 | .dropdown-submenu.pull-left { 38 | float: none; 39 | } 40 | 41 | .dropdown-submenu.pull-left > .dropdown-menu { 42 | left: -100%; 43 | margin-left: 10px; 44 | -webkit-border-radius: 6px 0 6px 6px; 45 | -moz-border-radius: 6px 0 6px 6px; 46 | border-radius: 6px 0 6px 6px; 47 | } 48 | 49 | .dropdown-submenu-right { 50 | position: relative; 51 | } 52 | 53 | .dropdown-submenu-right > .dropdown-menu { 54 | top: 0; 55 | right: 100%; 56 | margin-top: -6px; 57 | margin-left: -1px; 58 | -webkit-border-radius: 0; 59 | -moz-border-radius: 0; 60 | border-radius: 0; 61 | } 62 | 63 | .dropdown-submenu-right:hover > .dropdown-menu { 64 | display: block; 65 | } 66 | 67 | .dropdown-submenu-right > a:after { 68 | display: block; 69 | content: " "; 70 | float: right; 71 | width: 0; 72 | height: 0; 73 | border-color: transparent; 74 | border-style: solid; 75 | border-width: 5px 0 5px 5px; 76 | border-left-color: #ccc; 77 | margin-top: 5px; 78 | margin-right: -10px; 79 | } 80 | 81 | .dropdown-submenu-right:hover > a:after { 82 | border-left-color: #fff; 83 | } 84 | -------------------------------------------------------------------------------- /release/css/ui-navbar.min.css: -------------------------------------------------------------------------------- 1 | .dropdown-submenu,.dropdown-submenu-right{position:relative}.dropdown-submenu-right>a:after,.dropdown-submenu>a:after{display:block;content:" ";width:0;height:0;border-color:transparent transparent transparent #ccc;border-style:solid;border-width:5px 0 5px 5px;margin-right:-10px}.dropdown-submenu-right:hover>a:after,.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropdown-submenu>a:after{float:right;margin-top:5px}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown-submenu-right>.dropdown-menu{top:0;right:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.dropdown-submenu-right:hover>.dropdown-menu{display:block}.dropdown-submenu-right>a:after{float:right;margin-top:5px} -------------------------------------------------------------------------------- /release/js/ui-navbar.js: -------------------------------------------------------------------------------- 1 | angular.module('ui.navbar', ['ui.bootstrap', 'template/navbar-ul.html', 'template/navbar-li.html', 'template/navbar-tree-li.html']) 2 | 3 | .directive('trees', function () { 4 | 'use strict'; 5 | 6 | return { 7 | restrict: 'E', 8 | replace: true, 9 | scope: { 10 | trees: '=' 11 | }, 12 | templateUrl: 'template/navbar-tree-li.html' 13 | }; 14 | }) 15 | 16 | .directive('tree', function () { 17 | 'use strict'; 18 | 19 | return { 20 | restrict: 'E', 21 | replace: true, 22 | scope: { 23 | tree: '=' 24 | }, 25 | templateUrl: 'template/navbar-ul.html' 26 | }; 27 | }) 28 | 29 | .directive('leaf', ['$compile', function ($compile) { 30 | 'use strict'; 31 | 32 | return { 33 | restrict: 'E', 34 | replace: true, 35 | scope: { 36 | leaf: '=' 37 | }, 38 | templateUrl: 'template/navbar-li.html', 39 | link: function (scope, element) { 40 | if (angular.isArray(scope.leaf.subtree)) { 41 | element.append(''); 42 | 43 | // find the parent of the element 44 | var parent = element.parent(); 45 | var classFound = false; 46 | 47 | // check if in the hierarchy of the element exists a dropdown with class navbar-right 48 | while (parent.length > 0 && !classFound) { 49 | // check if the dropdown has been push to right 50 | if (parent.hasClass('navbar-right')) { 51 | classFound = true; 52 | } 53 | parent = parent.parent(); 54 | } 55 | 56 | // add a different class according to the position of the dropdown 57 | if (classFound) { 58 | element.addClass('dropdown-submenu-right'); 59 | } else { 60 | element.addClass('dropdown-submenu'); 61 | } 62 | 63 | $compile(element.contents())(scope); 64 | } 65 | } 66 | }; 67 | }]); 68 | 69 | angular.module("template/navbar-li.html", []).run(["$templateCache", function($templateCache) { 70 | $templateCache.put("template/navbar-li.html", 71 | "
  • \n" + 72 | " {{leaf.name}}\n" + 73 | "
  • "); 74 | }]); 75 | 76 | angular.module("template/navbar-tree-li.html", []).run(["$templateCache", function($templateCache) { 77 | $templateCache.put("template/navbar-tree-li.html", 78 | "
  • \n" + 79 | " {{tree.name}}\n" + 80 | " \n" + 81 | "
  • "); 82 | }]); 83 | 84 | angular.module("template/navbar-ul.html", []).run(["$templateCache", function($templateCache) { 85 | $templateCache.put("template/navbar-ul.html", 86 | ""); 89 | }]); 90 | -------------------------------------------------------------------------------- /release/js/ui-navbar.min.js: -------------------------------------------------------------------------------- 1 | angular.module("ui.navbar",["ui.bootstrap","template/navbar-ul.html","template/navbar-li.html","template/navbar-tree-li.html"]).directive("trees",function(){"use strict";return{restrict:"E",replace:!0,scope:{trees:"="},templateUrl:"template/navbar-tree-li.html"}}).directive("tree",function(){"use strict";return{restrict:"E",replace:!0,scope:{tree:"="},templateUrl:"template/navbar-ul.html"}}).directive("leaf",["$compile",function(a){"use strict";return{restrict:"E",replace:!0,scope:{leaf:"="},templateUrl:"template/navbar-li.html",link:function(b,c){if(angular.isArray(b.leaf.subtree)){c.append('');for(var d=c.parent(),e=!1;d.length>0&&!e;)d.hasClass("navbar-right")&&(e=!0),d=d.parent();e?c.addClass("dropdown-submenu-right"):c.addClass("dropdown-submenu"),a(c.contents())(b)}}}}]),angular.module("template/navbar-li.html",[]).run(["$templateCache",function(a){a.put("template/navbar-li.html",'
  • \n {{leaf.name}}\n
  • ')}]),angular.module("template/navbar-tree-li.html",[]).run(["$templateCache",function(a){a.put("template/navbar-tree-li.html",'')}]),angular.module("template/navbar-ul.html",[]).run(["$templateCache",function(a){a.put("template/navbar-ul.html","")}]); -------------------------------------------------------------------------------- /src/navbar.js: -------------------------------------------------------------------------------- 1 | angular.module('ui.navbar', ['ui.bootstrap', 'template/navbar-ul.html', 'template/navbar-li.html', 'template/navbar-tree-li.html']) 2 | 3 | .directive('trees', function () { 4 | 'use strict'; 5 | 6 | return { 7 | restrict: 'E', 8 | replace: true, 9 | scope: { 10 | trees: '=' 11 | }, 12 | templateUrl: 'template/navbar-tree-li.html' 13 | }; 14 | }) 15 | 16 | .directive('tree', function () { 17 | 'use strict'; 18 | 19 | return { 20 | restrict: 'E', 21 | replace: true, 22 | scope: { 23 | tree: '=' 24 | }, 25 | templateUrl: 'template/navbar-ul.html' 26 | }; 27 | }) 28 | 29 | .directive('leaf', ['$compile', function ($compile) { 30 | 'use strict'; 31 | 32 | return { 33 | restrict: 'E', 34 | replace: true, 35 | scope: { 36 | leaf: '=' 37 | }, 38 | templateUrl: 'template/navbar-li.html', 39 | link: function (scope, element) { 40 | if (angular.isArray(scope.leaf.subtree)) { 41 | element.append(''); 42 | 43 | // find the parent of the element 44 | var parent = element.parent(); 45 | var classFound = false; 46 | 47 | // check if in the hierarchy of the element exists a dropdown with class navbar-right 48 | while (parent.length > 0 && !classFound) { 49 | // check if the dropdown has been push to right 50 | if (parent.hasClass('navbar-right')) { 51 | classFound = true; 52 | } 53 | parent = parent.parent(); 54 | } 55 | 56 | // add a different class according to the position of the dropdown 57 | if (classFound) { 58 | element.addClass('dropdown-submenu-right'); 59 | } else { 60 | element.addClass('dropdown-submenu'); 61 | } 62 | 63 | $compile(element.contents())(scope); 64 | } 65 | } 66 | }; 67 | }]); 68 | -------------------------------------------------------------------------------- /template/navbar-li.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{leaf.name}} 3 |
  • -------------------------------------------------------------------------------- /template/navbar-li.html.js: -------------------------------------------------------------------------------- 1 | angular.module("template/navbar-li.html", []).run(["$templateCache", function($templateCache) { 2 | $templateCache.put("template/navbar-li.html", 3 | "
  • \n" + 4 | " {{leaf.name}}\n" + 5 | "
  • "); 6 | }]); 7 | -------------------------------------------------------------------------------- /template/navbar-tree-li.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/navbar-tree-li.html.js: -------------------------------------------------------------------------------- 1 | angular.module("template/navbar-tree-li.html", []).run(["$templateCache", function($templateCache) { 2 | $templateCache.put("template/navbar-tree-li.html", 3 | "
  • \n" + 4 | " {{tree.name}}\n" + 5 | " \n" + 6 | "
  • "); 7 | }]); 8 | -------------------------------------------------------------------------------- /template/navbar-ul.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/navbar-ul.html.js: -------------------------------------------------------------------------------- 1 | angular.module("template/navbar-ul.html", []).run(["$templateCache", function($templateCache) { 2 | $templateCache.put("template/navbar-ul.html", 3 | ""); 6 | }]); 7 | -------------------------------------------------------------------------------- /test/ui-navbar.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ui-navbar', function () { 4 | 5 | var $compile, scope; 6 | 7 | beforeEach(module('ui.navbar')); 8 | beforeEach(module('template/navbar-ul.html')); 9 | beforeEach(module('template/navbar-li.html')); 10 | beforeEach(module('template/navbar-tree-li.html')); 11 | 12 | beforeEach(inject(function (_$compile_, _$rootScope_) { 13 | scope = _$rootScope_.$new(); 14 | $compile = _$compile_; 15 | /* 16 | scope.tree = [{ 17 | name: "States", 18 | link: "#", 19 | subtree: [{ 20 | name: "state 1", 21 | link: "state1" 22 | }, { 23 | name: "state 2", 24 | link: "state2", 25 | subtree: [{ 26 | name: "state unknown", 27 | link: "state unknown 2" 28 | }] 29 | }] 30 | }];*/ 31 | })); 32 | 33 | function compileTemplate(template) { 34 | var el = $compile(angular.element(template))(scope); 35 | scope.$digest(); 36 | return el; 37 | } 38 | 39 | function createUiNavbar() { 40 | return compileTemplate( 41 | '
  • \ 42 | Dropdown \ 43 | \ 44 | \ 45 | \ 46 |
  • ' 47 | ) 48 | } 49 | 50 | // Unit tests 51 | describe('simple structure with only one element', function () { 52 | var element; 53 | 54 | beforeEach(function () { 55 | scope.tree = [{ 56 | name: "States", 57 | link: "#" 58 | }]; 59 | element = createUiNavbar(); 60 | }); 61 | 62 | it('should have one element ul', function () { 63 | // check there is an element ul with a specific class name 64 | expect(element.find('ul')).toBeDefined(); 65 | expect(element.find('ul').hasClass('dropdown-menu')).toBeTruthy(); 66 | expect(element.find('ul').children().length).toBe(1); 67 | expect(element.find('ul').parent().hasClass('dropdown')).toBe(true); 68 | }); 69 | 70 | it('should have one element li as parent of ul element having class dropdown', function () { 71 | expect(element.find('ul').parent().hasClass('dropdown')).toBe(true); 72 | }); 73 | 74 | it('should have an element a inside ul.li', function () { 75 | expect(element.find('li')).toBeDefined(); 76 | expect(element.find('li').find('a').attr('ui-sref')).toBe('#'); 77 | expect(element.find('li').find('a').text()).toBe('States'); 78 | }); 79 | }); 80 | 81 | // better to separate in another test, step by step 82 | describe('complex structure with subtree in a subtree', function () { 83 | var element; 84 | 85 | beforeEach(function () { 86 | scope.tree = [{ 87 | name: "States", 88 | link: "#", 89 | subtree: [{ 90 | name: "state 1", 91 | link: "state1" 92 | }, { 93 | name: "state 2", 94 | link: "state2", 95 | subtree: [{ 96 | name: "state unknown", 97 | link: "state unknown 2" 98 | }] 99 | }] 100 | }]; 101 | element = createUiNavbar(); 102 | }); 103 | 104 | it('should have the most external
  • element having class dropdown', function () { 105 | // most external
  • element has class dropdown 106 | expect(element.hasClass('dropdown')).toBeTruthy(); 107 | }); 108 | 109 | it('should have two