├── README.md ├── css └── navbar.style.css ├── html └── navbar.template.html └── js ├── navbar.decorator.js ├── navbar.directive.js ├── navbar.module.js ├── navbar.permission.js └── navbar.provider.js /README.md: -------------------------------------------------------------------------------- 1 | # angular-navbar 2 | 3 | ### Description 4 | 5 | An angular directive based on [ui-router](https://github.com/angular-ui/ui-router), [angular-permission](https://github.com/Narzerus/angular-permission) and [Bootstrap 3](http://getbootstrap.com/) for fast creation of responsive multi-level self-registered navigation bar. 6 | 7 | ### Usage 8 | 9 | Clone **angular-navbar** from repository to your **components** folder. If you do not have components folder in your project - create it. From the command line navigate to **components** folder and run 10 | ```sh 11 | git clone https://github.com/EugeneSnihovsky/angular-navbar 12 | ``` 13 | Add **navbar** to an array of dependences of angular 14 | ```javascript 15 | angular.module('yourAppName', [ 16 | 'ui.router', 17 | 'permission', 18 | 'navbar' 19 | ]); 20 | ``` 21 | You can register your state in navbar menu now. Just add field **menu** to object of state params, when you register a new state. 22 | 23 | Description of fields in menu object: 24 | * **name** - name of the future menu item (string) 25 | * **priority** - sort priority of items in the menu (number) 26 | * **location** - optional field, an object with two fields: 27 | * **place** - an array of names (string) that will build submenu 28 | * **priority** - an array of numeric priority for sorting submenus 29 | 30 | Field **permissions** declared similarly as in the module [angular-permission](https://github.com/Narzerus/angular-permission). 31 | 32 | Example for declaring state **page1** with item name **First page** on the first level of menu 33 | 34 | ```javascript 35 | angular.module('yourApp', []) 36 | .config(function ($stateProvider) { 37 | $stateProvider 38 | .state('page1', { 39 | url: '/page1', 40 | templateUrl: 'app/page1/page1.html', 41 | menu: { 42 | name: 'First page', 43 | priority: 40 44 | } 45 | }); 46 | }); 47 | ``` 48 | Example for declaring state **page2** with item name **my page** which will be located at submenu **page2 => page4 => page3** 49 | 50 | ```javascript 51 | angular.module('yourApp', []) 52 | .config(function ($stateProvider) { 53 | $stateProvider 54 | .state('page2', { 55 | url: '/page2', 56 | templateUrl: 'app/page2/page2.html', 57 | menu: { 58 | name: 'my page', 59 | priority: 20, 60 | location: { 61 | place: ['page2', 'page4', 'page3'], 62 | priority: [20, 10, 50] 63 | } 64 | } 65 | }); 66 | }); 67 | ``` 68 | Adding **permission field** to display in the menu, only those items for which the user has access 69 | ```javascript 70 | angular.module('yourApp', []) 71 | .config(function ($stateProvider) { 72 | $stateProvider 73 | .state('page2', { 74 | url: '/page2', 75 | templateUrl: 'app/page2/page2.html', 76 | data: { 77 | permissions: { 78 | except: ['anon', 'wrongPass'], 79 | redirectTo: 'page1' 80 | } 81 | }, 82 | menu: { 83 | name: 'my page', 84 | priority: 20, 85 | location: { 86 | place: ['page2', 'page4', 'page3'], 87 | priority: [20, 10, 50] 88 | } 89 | } 90 | }); 91 | }); 92 | ``` 93 | or 94 | ```javascript 95 | angular.module('yourApp', []) 96 | .config(function ($stateProvider) { 97 | $stateProvider 98 | .state('page1', { 99 | url: '/page1', 100 | templateUrl: 'app/page1/page1.html', 101 | data: { 102 | permissions: { 103 | only: ['user1', 'user2'], 104 | redirectTo: 'page2' 105 | } 106 | }, 107 | menu: { 108 | name: 'First page', 109 | priority: 40 110 | } 111 | }); 112 | }); 113 | ``` 114 | 115 | ### Known Issues 116 | 117 | 1. Directive does not work without jQuery library. 118 | -------------------------------------------------------------------------------- /css/navbar.style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Gloria+Hallelujah); 2 | 3 | .navbar-brand { 4 | font-family: "Gloria Hallelujah", Verdana, Tahoma; 5 | font-size: 23px; 6 | } 7 | 8 | .sub-menu { 9 | background-color: #333; 10 | } 11 | 12 | .sub-menu>a { 13 | color: #9d9d9d !important; 14 | padding-left: 10px !important; 15 | } 16 | 17 | .dropdown-menu { 18 | padding: 0px; 19 | margin-left: -1px; 20 | margin-right: -1px; 21 | min-width: 90px !important; 22 | } 23 | 24 | .dropdown-submenu { 25 | position:relative; 26 | } 27 | .dropdown-submenu>.dropdown-menu { 28 | top:0; 29 | right:100%; 30 | margin-top:6px; 31 | margin-left:-1px; 32 | -webkit-border-radius:0 6px 6px 6px; 33 | -moz-border-radius:0 6px 6px 6px; 34 | border-radius:0 6px 6px 6px; 35 | } 36 | 37 | .dropdown-submenu:hover>a:after { 38 | border-left-color:#ffffff; 39 | } 40 | .dropdown-submenu.pull-left { 41 | float:none; 42 | } 43 | .dropdown-submenu.pull-left>.dropdown-menu { 44 | left:-100%; 45 | margin-left:10px; 46 | -webkit-border-radius:6px 0 6px 6px; 47 | -moz-border-radius:6px 0 6px 6px; 48 | border-radius:6px 0 6px 6px; 49 | } 50 | 51 | .dropdown-submenu>a:before { 52 | display:block; 53 | content:" "; 54 | float:left; 55 | width: 0; 56 | height: 0; 57 | border-style: solid; 58 | border-color: transparent #cccccc transparent transparent; 59 | margin-top: 7px; 60 | margin-left: -5px; 61 | margin-right: 10px; 62 | } 63 | 64 | .dropdown-submenu-big>a:before { 65 | border-width: 4.5px 7.8px 4.5px 0; 66 | } 67 | 68 | .dropdown-submenu-small>a:before { 69 | margin-right: 7px; 70 | border-left: 5px solid transparent; 71 | border-right: 5px solid transparent; 72 | border-top: 5px solid #cccccc; 73 | } 74 | 75 | .dropdown-menu:hover, 76 | .dropdown-toggle:focus, 77 | li>[aria-expanded="true"], 78 | .navbar-brand:hover, 79 | .sub-menu>a:hover, 80 | .list-status:hover, 81 | .nav .open > a { 82 | color: #fff !important; 83 | background-color: #004444 !important; 84 | } 85 | 86 | .menu-active, 87 | .menu-active>a { 88 | font-weight: bold !important; 89 | text-decoration: underline; 90 | } 91 | 92 | .navbar-cheat { 93 | width: 100%; 94 | height: 45px; 95 | } 96 | 97 | 98 | .sub-link:before { 99 | display:block; 100 | content:" "; 101 | float:left; 102 | width: 12px; 103 | height: 5px; 104 | } 105 | 106 | /* Kukuri */ 107 | .link-kukuri { 108 | font-family: "Gloria Hallelujah"; 109 | outline: none; 110 | text-decoration: none !important; 111 | position: relative; 112 | font-size: 23px; 113 | line-height: 2; 114 | color: #c5c2b8; 115 | display: inline-block; 116 | } 117 | 118 | .link-kukuri:hover { 119 | color: #c5c2b8; 120 | } 121 | 122 | .link-kukuri:hover::after { 123 | -webkit-transform: translate3d(100%,0,0); 124 | transform: translate3d(100%,0,0); 125 | } 126 | 127 | .link-kukuri::before { 128 | content: attr(data-letters); 129 | position: absolute; 130 | z-index: 2; 131 | overflow: hidden; 132 | color: #424242; 133 | white-space: nowrap; 134 | width: 0%; 135 | -webkit-transition: width 0.4s 0.0s; 136 | transition: width 0.4s 0.0s; 137 | } 138 | 139 | .link-kukuri:hover::before { 140 | width: 100%; 141 | } 142 | 143 | .link-kukuri:focus { 144 | color: #9e9ba4; 145 | } -------------------------------------------------------------------------------- /html/navbar.template.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 12 | 24 |
25 | 26 | 37 | 38 | -------------------------------------------------------------------------------- /js/navbar.decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | angular.module('navbar') 5 | .config(function ($stateProvider, navbarListProvider) { 6 | // добавляем в метод state функционал регистрации пунктов меню 7 | $stateProvider.decorator('state', function (obj) { 8 | var menu = obj.menu, 9 | permissions = (obj.data) ? obj.data.permissions : null; 10 | // если в коде не указана регистрация текущего стейта в меню - ничего не делаем 11 | if(!menu) { 12 | return; 13 | } 14 | menu.state = obj.name; 15 | // регистрируем права доступа пункта при их наличии 16 | if(permissions) { 17 | menu.permissions = {}; 18 | if(permissions.except) { 19 | menu.permissions.except = permissions.except; 20 | } else if(permissions.only) { 21 | menu.permissions.only = permissions.only; 22 | } else { 23 | delete menu.permissions; 24 | } 25 | } 26 | // регистрируем пункт меню по скомпонованному объекту menu 27 | navbarListProvider.add(menu); 28 | }); 29 | }); 30 | })(); 31 | -------------------------------------------------------------------------------- /js/navbar.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function () { 4 | angular.module('navbar') 5 | .directive('navbar', function ($document, $state, navbarList, navPermission) { 6 | return { 7 | restrict: 'A', 8 | scope: { 9 | name: '@', 10 | sref: '@' 11 | }, 12 | templateUrl: '/components/angular-navbar/html/navbar.template.html', 13 | link: function (scope, elem) { 14 | var openedMenu = null, 15 | openedSubMenu = null, 16 | username = navPermission.getUser($state.params); 17 | // присваиваем нашему DOM элементу необходимые классы и атрибуты для работы bootstrap 18 | elem.addClass('navbar navbar-inverse navbar-fixed-top'); 19 | elem.attr('role', 'navigation'); 20 | // редактируем список пунктов меню в соотвествии с доступом и передаем его в scope директивы 21 | if(username) { 22 | navPermission.acceptPermission(navbarList.list, username); 23 | } 24 | scope.navbar = navbarList.list; 25 | // открытие/сокрытие меню на телефонах или при узком экране браузера 26 | scope.collapseMenu = function ($event) { 27 | var navbar = elem.find('#navbar'), 28 | expanded = navbar.hasClass('in'); 29 | 30 | navbar.attr('aria-expanded', !expanded); 31 | scope.navCollapsed = (expanded) ? '' : 'in'; 32 | 33 | closeAllMenu(); 34 | stopBubleAndPropagation($event); 35 | }; 36 | // присвоение класса активного пункта меню соответствующей страницы и класса подменю, если пункт содержит подпункты 37 | scope.menuClass = function (item, level) { 38 | var status = false, 39 | activePage = getActivePage($state.current.name), 40 | currentPage = (item.pop) ? item[0] : item, 41 | classList = (level === 'firstLevel') ? 'dropdown dropdown-firstLevel ' : 42 | 'menu-item dropdown dropdown-submenu ', 43 | activeClass = (currentPage === activePage || isActive(item, activePage, status) ) ? 44 | 'menu-active' : ''; 45 | 46 | if(item.pop) { 47 | return classList + activeClass; 48 | } else { 49 | return activeClass; 50 | } 51 | }; 52 | // получение имени активного пункта меню в соответствии с открытой страницей (состоянием) 53 | function getActivePage(state, currentList) { 54 | var name; 55 | 56 | if(!currentList) { 57 | currentList = scope.navbar; 58 | } 59 | 60 | for(var i = (currentList[0].name) ? 0 : 1; i < currentList.length; i++) { 61 | if(currentList[i].state === state) { 62 | return currentList[i].name; 63 | } else if(currentList[i].name.pop) { 64 | name = getActivePage(state, currentList[i].name); 65 | } 66 | } 67 | return name; 68 | } 69 | // проверка, является ли пункт меню активным 70 | function isActive (item, activePage, status) { 71 | if(item.pop) { 72 | for(var i = 1; i < item.length; i++) { 73 | if(item[i].name.pop) { 74 | status = isActive(item[i].name, activePage, status); 75 | } else if(item[i].name === activePage) { 76 | return true; 77 | } 78 | } 79 | } else if(item === activePage) { 80 | return true; 81 | } 82 | return status; 83 | } 84 | // раскрытие сокрытие подпунктов меню по кликку или наведению мыши (страшная функция, т.к. учтены варианты разного разрешения экрана) 85 | scope.expandMenu = function ($event) { 86 | var clickedElem = $($event.currentTarget), 87 | parentClicked = $($event.currentTarget.parentElement), 88 | expanded = clickedElem.attr('aria-expanded'), 89 | isOpened = parentClicked.hasClass('open'), 90 | attrExpanded = (expanded === 'false'), 91 | allOpenedMenu = parentClicked.parent().find('.open'), 92 | smallWindow = window.innerWidth < 768, 93 | eventMouseEnter = $event.type === 'mouseenter', 94 | subMenuAll = elem.find('.dropdown-submenu'); 95 | 96 | if(!smallWindow || !eventMouseEnter) { 97 | allOpenedMenu.removeClass('open'); 98 | clickedElem.attr('aria-expanded', attrExpanded); 99 | 100 | if(isOpened && !eventMouseEnter) { 101 | parentClicked.removeClass('open'); 102 | } else { 103 | parentClicked.addClass('open'); 104 | openedMenu = clickedElem; //** 105 | } 106 | } 107 | 108 | subMenuAll.removeClass('dropdown-submenu-small dropdown-submenu-big'); 109 | if(smallWindow) { 110 | subMenuAll.addClass('dropdown-submenu-small'); 111 | } else { 112 | subMenuAll.addClass('dropdown-submenu-big'); 113 | } 114 | stopBubleAndPropagation($event); 115 | }; 116 | // закрытие подменю при наведении на соседний пункт в основном меню 117 | scope.closeOnMoveMenu = function () { 118 | var smallWindow = window.innerWidth < 768; 119 | 120 | if(openedMenu && !smallWindow) { 121 | var clickedLink = openedMenu, 122 | clickedElement = clickedLink.parent(); 123 | 124 | clickedElement.removeClass('open'); 125 | clickedLink.attr('aria-expanded', false); 126 | openedMenu = null; 127 | } 128 | }; 129 | // раскрытие сокрытие подпунктов подменю (аналогично функции с 92 строки) 130 | scope.expandSubMenu = function ($event) { 131 | var elemClicked = $($event.currentTarget.parentElement), 132 | smallWindow = window.innerWidth < 768, 133 | eMouseEnter = $event.type === 'mouseenter', 134 | sameElement = elemClicked.hasClass('open'); 135 | 136 | if(!smallWindow || !eMouseEnter) { // потом подумать как упростить 137 | if(!sameElement && !eMouseEnter || !eMouseEnter || !sameElement) { 138 | elemClicked.parent().find('.open').removeClass('open'); 139 | } 140 | if(!sameElement) { 141 | elemClicked.addClass('open'); 142 | openedSubMenu = elemClicked; 143 | } 144 | } 145 | stopBubleAndPropagation($event); 146 | }; 147 | // закрытие подменю при наведении на соседний подпункт в подменю (звучит то как:)) 148 | scope.closeOnMoveSubMenu = function ($event) { 149 | var smallWindow = window.innerWidth < 768; 150 | 151 | if(openedSubMenu && !smallWindow) { 152 | var clickedElement = openedSubMenu, 153 | savedList = clickedElement.parent(), 154 | currentList = $($event.target).parent().parent(); 155 | 156 | if(savedList[0] === currentList[0]) { 157 | clickedElement.removeClass('open'); 158 | openedSubMenu = null; 159 | } 160 | } 161 | }; 162 | 163 | scope.closeMenu = closeMenu; 164 | // удаляем всех слушателей с документа при его уничтожении 165 | var $body = $document.find('html'); 166 | elem.bind('$destroy', function() { 167 | $body.unbind(); //не хватает проверки на удаленный элемент 168 | }); 169 | // при клике вне меню - закрываем все открытые позиции 170 | $body.bind('click', closeMenu); 171 | 172 | function closeMenu ($event) { 173 | var elemClicked = $event.relatedTarget || $event.target; 174 | 175 | if(isClickOutNavbar(elemClicked)) { 176 | closeAllMenu(); 177 | } 178 | } 179 | // рекурсивно поднимаемся по родителям элемента, чтобы узнать, был клик по меню или нет 180 | function isClickOutNavbar(elem) { 181 | if($(elem).hasClass('dropdown-firstLevel')) { 182 | return false; 183 | } 184 | 185 | if(elem.parentElement !== null) { 186 | return isClickOutNavbar(elem.parentElement); 187 | } else { 188 | return true; 189 | } 190 | } 191 | // закрываем все открытые пункты и подпункты меню 192 | function closeAllMenu() { 193 | elem.find('.open').removeClass('open'); 194 | elem.find('[aria-expanded=true]').attr('aria-expanded', false); 195 | } 196 | // служебная функция предотвращения действий браузера поумолчанию и всплывающих событий 197 | function stopBubleAndPropagation($event) { 198 | $event.stopPropagation(); 199 | $event.preventDefault(); 200 | } 201 | 202 | } 203 | }; 204 | }); 205 | })(); 206 | 207 | 208 | // попробовать перенести применение пермишна в секцию рун 209 | 210 | 211 | // добавить недоступный курсор при наведении на уже нажатую ссылку в меню 212 | // втулить $ jQuery зависимость в директиву, чтобы не подключать его 213 | // разбить сложную функцию клик+онмове, на две простые 214 | 215 | 216 | // сделать выбор через || откуда брать массив меню, из скоупа или из провайдера 217 | 218 | // попробовать прикрутить анимацию загрузки на каждую страницу в пунктах меню 219 | 220 | // отсортировать функции, моусемов + онклик + другие 221 | 222 | 223 | 224 | // сайт пункт хитрости ангуляра и js -------------------------------------------------------------------------------- /js/navbar.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function () { 4 | angular.module('navbar', ['ui.router']); 5 | })(); 6 | -------------------------------------------------------------------------------- /js/navbar.permission.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function () { 4 | angular.module('navbar') 5 | .factory('navPermission', function (Permission, $q) { 6 | // перебираем все роли и возвращаем подходящую в виде промиса 7 | function getUser(params) { 8 | var users = Permission.roleValidations, 9 | names = Object.keys(users), 10 | promisesArr = []; 11 | 12 | for(var i = 0; i < names.length; i++) { 13 | var current = names[i], 14 | validUser = $q.when( users[current](params) ); 15 | promisesArr.push(validUser); 16 | } 17 | 18 | return $q.all(promisesArr).then(function (users) { 19 | for(var i = 0; i < users.length; i++) { 20 | if(users[i]) { 21 | return names[i]; 22 | } 23 | } 24 | return null; 25 | }); 26 | } 27 | // если пришел промис, ждем его разрешения и меняем меню, если пользователь - сразу меняем меню 28 | function acceptPermission (list, username) { 29 | if(!username.then) { 30 | return changeList(list, username); 31 | } else { 32 | return username.then(function (username) { 33 | return changeList(list, username); 34 | }); 35 | } 36 | } 37 | // рекурсивно пробегаемся по массиву меню и удаляем пункты, которые запрещены для текущей роли 38 | function changeList(list, username) { 39 | for(var i = (list[0].name) ? 0 : 1; i < list.length; i++) { 40 | if(list[i].permissions) { 41 | if(list[i].permissions.except) { 42 | var except = list[i].permissions.except; 43 | 44 | for(var j = 0; j < except.length; j++) { 45 | if(except[j] === username) { 46 | list.splice(i--, 1); 47 | } 48 | } 49 | } else if(list[i].permissions.only) { 50 | var only = list[i].permissions.only, 51 | accessDenided = true; 52 | 53 | for(j = 0; j < only.length; j++) { 54 | if(only[j] === username) { 55 | accessDenided = false; 56 | } 57 | } 58 | if(accessDenided) { 59 | list.splice(i--, 1); 60 | } 61 | } 62 | } else if(list[i].name.pop) { 63 | list[i].name = changeList( list[i].name, username); 64 | if(list[i].name.length === 1 ) { 65 | list.splice(i--, 1); 66 | } 67 | } 68 | } 69 | 70 | return list; 71 | } 72 | // возвращаем созданные методы фабрики 73 | return { 74 | getUser: getUser, 75 | acceptPermission: acceptPermission 76 | }; 77 | }); 78 | })(); -------------------------------------------------------------------------------- /js/navbar.provider.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function () { 4 | angular.module('navbar') 5 | .provider('navbarList', function () { 6 | var list = []; 7 | // основная функция добавления пункта в меню 8 | this.add = function (obj) { 9 | // проверка на правильно заданные параметры расположения пункта 10 | if(obj.location) { 11 | if(obj.location.place.length !== obj.location.priority.length || 12 | !obj.location.place.pop || !obj.location.priority.pop) { 13 | console.log('Warning! Bad location params for menu "' + obj.name + '". Skip item'); 14 | return; 15 | } 16 | } 17 | // добавление пункта на первый уровень меню при отстутствии местоположения 18 | if(!obj.location) { 19 | var name = obj.name; 20 | for(var i = 0; i < list.length; i++) { // рассказать про тернарный оператор и утиную типизацию 21 | var currentName = (list[i].name.pop) ? list[i].name[0] : list[i].name; 22 | if(currentName === name) { 23 | console.log('Warning! Duplicate menu "' + name + '". Skip item'); 24 | return; 25 | } 26 | } 27 | list.push(obj); 28 | list.sort(sortByPriority); 29 | return; 30 | } 31 | // поиск пункта, в который нужно добавить подпункт согласно местоположению 32 | var place = obj.location.place.shift(), 33 | priority = obj.location.priority.shift(); 34 | 35 | for(i = 0; i < list.length; i++) { // описать в статье, что i блочная не в JS 36 | var currentSubName = (list[i].name.pop) ? list[i].name[0] : null; 37 | if(place === currentSubName) { 38 | list[i].name = changeExistPart(obj, list[i].name); 39 | if(priority !== list[i].priority) { 40 | console.log('Warning! Priority of menu "' + list[i].name + '" has been changed from "' + 41 | list[i].priority + '" to "' + priority + '"'); 42 | list[i].priority = priority; 43 | list.sort(sortByPriority); 44 | } 45 | return; 46 | } 47 | currentName = list[i].name; 48 | if(place === currentName) { 49 | console.log('Warning! Duplicate submenu "' + place + '". Skip item'); 50 | return; 51 | } 52 | } 53 | // ни одно вышеописанное условие не совпало, добавляем новый пункт со всеми вложениями 54 | list.push( { 55 | name: [place, makeOriginalPart(obj)], 56 | priority: priority 57 | } ); 58 | list.sort(sortByPriority); 59 | }; 60 | // рекурсивный поиск места в подпунктах меню для вставки нового пункта 61 | function changeExistPart(obj, list) { 62 | var place = obj.location.place.shift(), 63 | priority = obj.location.priority.shift(), // возможно необходимо сделать двойной приоритет 64 | searchName = (place) ? place : obj.name; 65 | 66 | for(var i = 1; i < list.length; i++) { 67 | var currentName = (list[i].name.pop) ? list[i].name[0] : list[i].name; 68 | if(searchName === currentName) { 69 | if(!list[i].name.pop || (!place && list[i].name.pop) ) { 70 | console.log('Warning! Duplicate menu "' + searchName + '". Skip item'); 71 | return list; 72 | } else { 73 | list[i].name = changeExistPart(obj, list[i].name); 74 | if(priority !== list[i].priority) { 75 | console.log('Warning! Priority of menu "' + list[i].name + 76 | '" has been changed from "' + list[i].priority + '" to "' + priority + '"'); 77 | list[i].priority = priority; 78 | list.sort(sortByPriority); 79 | } 80 | return list; 81 | } 82 | } 83 | } 84 | if(!place) { 85 | delete obj.location; 86 | list.push(obj); 87 | } else { 88 | list.push({ 89 | name: [place, makeOriginalPart(obj)], 90 | priority: priority 91 | }); 92 | } 93 | list.sort(sortByPriority); 94 | return list; 95 | } 96 | // рекурсивное создание новой, оригинальной части пункта меню с подпунктами 97 | function makeOriginalPart (obj) { 98 | var place = obj.location.place.shift(), 99 | priority = obj.location.priority.shift(); 100 | 101 | if(place) { 102 | var menu = { 103 | priority: priority, 104 | name: [place, makeOriginalPart(obj)] 105 | }; 106 | } else { 107 | delete obj.location; 108 | menu = obj; 109 | } 110 | return menu; 111 | } 112 | // функция сортировки пунктов меню по приоритету 113 | function sortByPriority(a, b) { 114 | return a.priority - b.priority; 115 | } 116 | // служебная функция для работы провайдера angularJS 117 | this.$get = function () { 118 | return { 119 | list: list, 120 | add: this.add 121 | }; 122 | }; 123 | }); 124 | })(); 125 | 126 | // сделать проверку на повторение стейтов в меню. Пушаем их все в один массив и прогоняем на совпадение 127 | --------------------------------------------------------------------------------