refactor(sidebar): change sidebar API and improve UX

Closes #14, #15
pull/217/head
Vladimir Lugovsky 2016-05-02 21:56:56 +03:00
parent c424ead837
commit 6f6e11904a
14 changed files with 300 additions and 173 deletions

View File

@ -0,0 +1,30 @@
/**
* @author v.lugovksy
* created on 16.12.2015
*/
(function () {
'use strict';
angular.module('BlurAdmin.theme.components')
.controller('BaSidebarCtrl', BaSidebarCtrl);
/** @ngInject */
function BaSidebarCtrl($scope, baSidebarService) {
$scope.menuItems = baSidebarService.getMenuItems();
$scope.defaultSidebarState = $scope.menuItems[0].stateRef;
$scope.hoverItem = function ($event) {
$scope.showHoverElem = true;
$scope.hoverElemHeight = $event.currentTarget.clientHeight;
var menuTopValue = 66;
$scope.hoverElemTop = $event.currentTarget.getBoundingClientRect().top - menuTopValue;
};
$scope.$on('$stateChangeSuccess', function () {
if (baSidebarService.canSidebarBeHidden()) {
baSidebarService.setMenuCollapsed(true);
}
});
}
})();

View File

@ -0,0 +1,42 @@
<aside class="al-sidebar" ng-swipe-right="$baSidebarService.setMenuCollapsed(false)" ng-swipe-left="$baSidebarService.setMenuCollapsed(true)"
ng-mouseleave="hoverElemTop=selectElemTop">
<ul class="al-sidebar-list" slimscroll="{height: '{{menuHeight}}px'}" slimscroll-watch="menuHeight" >
<li ng-repeat="item in ::menuItems" class="al-sidebar-list-item"
ng-class="::{'with-sub-menu': item.subMenu}" ui-sref-active="selected">
<a ng-mouseenter="hoverItem($event, item)" ui-sref="{{ ::(item.stateRef || defaultSidebarState) }}" ng-if="::!item.subMenu" class="al-sidebar-list-link">
<i class="{{ ::item.icon }}"></i><span>{{ ::item.title }}</span>
</a>
<a ng-mouseenter="hoverItem($event, item)" ng-if="::item.subMenu"
class="al-sidebar-list-link" ba-ui-sref-toggler="item.stateRef">
<i class="{{ ::item.icon }}"></i><span>{{ ::item.title }}</span>
<b class="fa fa-angle-down" ui-sref-active="fa-angle-up"
ng-if="::item.subMenu"></b>
</a>
<ul ng-if="::item.subMenu" class="al-sidebar-sublist"
ng-class="{'slide-right': item.slideRight}"
ba-ui-sref-toggling-submenu="item.stateRef">
<li ng-repeat="subitem in ::item.subMenu" ng-class="::{'with-sub-menu': subitem.subMenu}" ui-sref-active="selected" class="ba-sidebar-sublist-item">
<a ng-mouseenter="hoverItem($event, item)" ng-if="::subitem.subMenu" ba-ui-sref-toggler="subitem.stateRef"
class="al-sidebar-list-link subitem-submenu-link"><span>{{ ::subitem.title }}</span>
<b class="fa" ng-class="{'fa-angle-up': subitem.expanded, 'fa-angle-down': !subitem.expanded}"
ng-if="::subitem.subMenu"></b>
</a>
<ul ng-if="::subitem.subMenu" class="al-sidebar-sublist subitem-submenu-list"
ng-class="{expanded: subitem.expanded, 'slide-right': subitem.slideRight}"
ba-ui-sref-toggling-submenu="subitem.stateRef">
<li ng-mouseenter="hoverItem($event, item)" ng-repeat="subSubitem in ::subitem.subMenu" ui-sref-active="selected">
<a ng-mouseenter="hoverItem($event, item)" ui-sref="{{ ::(subSubitem.stateRef || defaultSidebarState) }}">{{
::subSubitem.title }}</a>
</li>
</ul>
<a ng-mouseenter="hoverItem($event, item)" target="{{::(subitem.blank ? '_blank' : '_self')}}" ng-if="::!subitem.subMenu" ui-sref="{{ ::(subitem.stateRef || defaultSidebarState) }}">{{ ::subitem.title}}</a>
</li>
</ul>
</li>
</ul>
<div class="sidebar-hover-elem" ng-style="{top: hoverElemTop + 'px', height: hoverElemHeight + 'px'}"
ng-class="{'show-hover-elem': showHoverElem }"></div>
</aside>

View File

@ -0,0 +1,60 @@
/**
* @author v.lugovksy
* created on 16.12.2015
*/
(function () {
'use strict';
angular.module('BlurAdmin.theme.components')
.directive('baSidebar', baSidebar);
/** @ngInject */
function baSidebar($timeout, baSidebarService, baUtil, layoutSizes) {
var jqWindow = $(window);
return {
restrict: 'E',
templateUrl: 'app/theme/components/baSidebar/ba-sidebar.html',
controller: 'BaSidebarCtrl',
link: function(scope, el) {
scope.menuHeight = el[0].childNodes[0].clientHeight - 84;
jqWindow.on('click', _onWindowClick);
jqWindow.on('resize', _onWindowResize);
scope.$on('$destroy', function() {
jqWindow.off('click', _onWindowClick);
jqWindow.off('resize', _onWindowResize);
});
function _onWindowClick($evt) {
if (!baUtil.isDescendant(el[0], $evt.target) &&
!$evt.originalEvent.$sidebarEventProcessed &&
!baSidebarService.isMenuCollapsed() &&
baSidebarService.canSidebarBeHidden()) {
$evt.originalEvent.$sidebarEventProcessed = true;
$timeout(function () {
baSidebarService.setMenuCollapsed(true);
}, 10);
}
}
// watch window resize to change menu collapsed state if needed
function _onWindowResize() {
var newMenuCollapsed = baSidebarService.shouldMenuBeCollapsed();
var newMenuHeight = _calculateMenuHeight();
if (newMenuCollapsed != baSidebarService.isMenuCollapsed() || scope.menuHeight != newMenuHeight) {
scope.$apply(function () {
scope.menuHeight = newMenuHeight;
baSidebarService.setMenuCollapsed(newMenuCollapsed)
});
}
}
function _calculateMenuHeight() {
return el[0].childNodes[0].clientHeight - 84;
}
}
};
}
})();

View File

@ -2,10 +2,13 @@
'use strict';
angular.module('BlurAdmin.theme.components')
.service('sidebarService', sidebarService);
.service('baSidebarService', baSidebarService);
/** @ngInject */
function sidebarService($state) {
function baSidebarService($state, layoutSizes) {
var isMenuCollapsed = shouldMenuBeCollapsed();
var staticMenuItems = [ {
title: 'Pages',
icon: 'ion-document',
@ -58,6 +61,21 @@
return menuItems.concat(staticMenuItems);
};
this.shouldMenuBeCollapsed = shouldMenuBeCollapsed;
this.canSidebarBeHidden = canSidebarBeHidden;
this.setMenuCollapsed = function(isCollapsed) {
isMenuCollapsed = isCollapsed;
};
this.isMenuCollapsed = function() {
return isMenuCollapsed;
};
this.toggleMenuCollapsed = function() {
isMenuCollapsed = !isMenuCollapsed;
};
function defineMenuItemStates() {
return $state.get()
.filter(function(s) {
@ -71,12 +89,20 @@
level: (s.name.match(/\./g) || []).length,
order: meta.order,
icon: meta.icon,
root: '#/' + s.name.replace('.', '/'),
stateRef: s.name,
};
})
.sort(function(a, b) {
return (a.level - b.level) * 100 + a.order - b.order;
});
}
function shouldMenuBeCollapsed() {
return window.innerWidth <= layoutSizes.resWidthCollapseSidebar;
}
function canSidebarBeHidden() {
return window.innerWidth <= layoutSizes.resWidthHideSidebar;
}
}
})();

View File

@ -0,0 +1,91 @@
/**
* @author v.lugovsky
* created on 03.05.2016
*/
(function () {
'use strict';
angular.module('BlurAdmin.theme.components')
.directive('baSidebarToggleMenu', baSidebarToggleMenu)
.directive('baSidebarCollapseMenu', baSidebarCollapseMenu)
.directive('baUiSrefTogglingSubmenu', baUiSrefTogglingSubmenu)
.directive('baUiSrefToggler', baUiSrefToggler);
/** @ngInject */
function baSidebarToggleMenu(baSidebarService) {
return {
restrict: 'A',
link: function(scope, elem) {
elem.on('click', function($evt) {
$evt.originalEvent.$sidebarEventProcessed = true;
scope.$apply(function() {
baSidebarService.toggleMenuCollapsed();
});
});
}
};
}
/** @ngInject */
function baSidebarCollapseMenu(baSidebarService) {
return {
restrict: 'A',
link: function(scope, elem) {
elem.on('click', function($evt) {
$evt.originalEvent.$sidebarEventProcessed = true;
if (!baSidebarService.isMenuCollapsed()) {
scope.$apply(function() {
baSidebarService.setMenuCollapsed(true);
});
}
});
}
};
}
/** @ngInject */
function baUiSrefTogglingSubmenu($state) {
return {
restrict: 'A',
link: function(scope, el, attrs) {
var stateToWatch = scope.$eval(attrs.baUiSrefTogglingSubmenu);
if (_isState($state.current)) {
el.parent().addClass('ba-sidebar-item-expanded');
}
scope.$on('$stateChangeStart', function (event, toState) {
if (!_isState(toState) && el.parent().hasClass('ba-sidebar-item-expanded')) {
el.slideToggle();
el.parent().removeClass('ba-sidebar-item-expanded');
}
});
scope.$on('$stateChangeSuccess', function (event, toState) {
if (_isState(toState) && !el.parent().hasClass('ba-sidebar-item-expanded')) {
el.slideToggle();
el.parent().addClass('ba-sidebar-item-expanded');
}
});
function _isState(state) {
return state && state.name.indexOf(stateToWatch) == 0;
}
}
};
}
/** @ngInject */
function baUiSrefToggler() {
return {
restrict: 'A',
link: function(scope, el, attrs) {
el.on('click', function() {
el.next().slideToggle();
el.parent().toggleClass('ba-sidebar-item-expanded');
});
}
};
}
})();

View File

@ -12,9 +12,6 @@
function pageTop() {
return {
restrict: 'E',
scope: {
isMenuCollapsed: '=',
},
templateUrl: 'app/theme/components/pageTop/pageTop.html'
};
}

View File

@ -1,6 +1,6 @@
<div class="page-top clearfix" scroll-position="scrolled" max-height="50" ng-class="{'scrolled': scrolled}">
<a href="#/dashboard" class="al-logo clearfix"><span>Blur</span>Admin</a>
<a href class="collapse-menu-link ion-navicon" ng-click="isMenuCollapsed=!isMenuCollapsed"></a>
<a href class="collapse-menu-link ion-navicon" ba-sidebar-toggle-menu></a>
<div class="search">
<i class="ion-ios-search-strong" ng-click="startSearch()"></i>

View File

@ -1,101 +0,0 @@
/**
* @author v.lugovksy
* created on 16.12.2015
*/
(function () {
'use strict';
angular.module('BlurAdmin.theme.components')
.controller('SidebarCtrl', SidebarCtrl);
/** @ngInject */
function SidebarCtrl($scope, $rootScope, $timeout, $location, layoutSizes, sidebarService, $element) {
$scope.menuItems = sidebarService.getMenuItems();
$scope.menuHeight = $element[0].childNodes[0].clientHeight - 84;
function selectMenuItem() {
$.each($scope.menuItems, function (index, menu) {
menu.selected = ('#' + $location.$$url).indexOf(menu.root) == 0;
menu.expanded = menu.expanded || menu.selected;
if (menu.subMenu) {
$.each(menu.subMenu, function (subIndex, subMenu) {
subMenu.selected = (('#' + $location.$$url).indexOf(subMenu.root) == 0) && !subMenu.disabled;
});
}
});
}
selectMenuItem();
$scope.$on('$locationChangeSuccess', function () {
selectMenuItem();
});
$scope.menuExpand = function () {
$scope.$isMenuCollapsed = false;
};
$scope.menuCollapse = function () {
$scope.$isMenuCollapsed = true;
};
// watch window resize to change menu collapsed state if needed
$(window).resize(function () {
var isMenuShouldCollapsed = $(window).width() <= layoutSizes.resWidthCollapseSidebar;
var scopeApplied = false;
if ($scope.isMenuShouldCollapsed !== isMenuShouldCollapsed) {
$scope.$apply(function () {
$scope.menuHeight = $element[0].childNodes[0].clientHeight - 84;
$scope.$isMenuCollapsed = isMenuShouldCollapsed;
scopeApplied = true;
});
}
if (!scopeApplied) {
$scope.$apply(function () {
$scope.menuHeight = $element[0].childNodes[0].clientHeight - 84;
});
}
$scope.isMenuShouldCollapsed = isMenuShouldCollapsed;
});
$scope.toggleSubMenu = function ($event, item) {
var submenu = $($event.currentTarget).next();
if ($scope.$isMenuCollapsed) {
$scope.menuExpand();
if (!item.expanded) {
$timeout(function () {
item.expanded = !item.expanded;
submenu.slideToggle();
});
}
} else {
item.expanded = !item.expanded;
submenu.slideToggle();
}
};
window.onclick = function () {
$timeout(function () {
if ($scope.anySlideRight) {
$scope.menuItems.map(function (val) {
return val.slideRight = false;
});
$scope.anySlideRight = false;
}
}, 10);
};
$scope.hoverItem = function ($event) {
$scope.showHoverElem = true;
$scope.hoverElemHeight = $event.currentTarget.clientHeight;
var menuTopValue = 66;
$scope.hoverElemTop = $event.currentTarget.getBoundingClientRect().top - menuTopValue;
};
}
})();

View File

@ -1,20 +0,0 @@
/**
* @author v.lugovksy
* created on 16.12.2015
*/
(function () {
'use strict';
angular.module('BlurAdmin.theme.components')
.directive('sidebar', sidebar);
/** @ngInject */
function sidebar() {
return {
restrict: 'E',
templateUrl: 'app/theme/components/sidebar/sidebar.html',
controller: 'SidebarCtrl'
};
}
})();

View File

@ -1,40 +0,0 @@
<aside class="al-sidebar" ng-swipe-right="menuExpand()" ng-swipe-left="menuCollapse()"
ng-mouseleave="hoverElemTop=selectElemTop">
<ul class="al-sidebar-list" slimscroll="{height: '{{menuHeight}}px'}" slimscroll-watch="menuHeight" >
<li ng-repeat="item in menuItems" class="al-sidebar-list-item"
ng-class="{'selected': item.selected, 'with-sub-menu': item.subMenu}">
<a ng-mouseenter="hoverItem($event, item)" href="{{ item.root }}" ng-if="!item.subMenu" class="al-sidebar-list-link">
<i class="{{ item.icon }}"></i><span>{{ item.title }}</span>
</a>
<a ng-mouseenter="hoverItem($event, item)" ng-if="item.subMenu" href ng-click="toggleSubMenu($event, item)"
class="al-sidebar-list-link">
<i class="{{ item.icon }}"></i><span>{{ item.title }}</span>
<b class="fa" ng-class="{'fa-angle-up': item.expanded, 'fa-angle-down': !item.expanded}"
ng-if="item.subMenu"></b>
</a>
<ul ng-if="item.subMenu" class="al-sidebar-sublist"
ng-class="{expanded: item.expanded, 'slide-right': item.slideRight}">
<li ng-repeat="subitem in item.subMenu" ng-class="{'selected': subitem.selected, 'with-sub-menu': subitem.subMenu}">
<a ng-mouseenter="hoverItem($event, item)" ng-if="subitem.subMenu" href ng-click="toggleSubMenu($event, subitem);"
class="al-sidebar-list-link subitem-submenu-link"><span>{{ subitem.title }}</span>
<b class="fa" ng-class="{'fa-angle-up': subitem.expanded, 'fa-angle-down': !subitem.expanded}"
ng-if="subitem.subMenu"></b>
</a>
<ul ng-if="subitem.subMenu" class="al-sidebar-sublist subitem-submenu-list"
ng-class="{expanded: subitem.expanded, 'slide-right': subitem.slideRight}">
<li ng-mouseenter="hoverItem($event, item)" ng-repeat="subSubitem in subitem.subMenu" ng-class="{selected: subitem.selected}">
<a ng-mouseenter="hoverItem($event, item)" href="{{ subSubitem.root }}">{{
subSubitem.title }}</a>
</li>
</ul>
<a ng-mouseenter="hoverItem($event, item)" target="{{subitem.blank ? '_blank' : '_self'}}" ng-if="!subitem.subMenu" href="{{ subitem.root }}">{{ subitem.title}}</a>
</li>
</ul>
</li>
</ul>
<div class="sidebar-hover-elem" ng-style="{top: hoverElemTop + 'px', height: hoverElemHeight + 'px'}"
ng-class="{'show-hover-elem': showHoverElem }"></div>
</aside>

View File

@ -0,0 +1,27 @@
/**
* @author v.lugovsky
* created on 03.05.2016
*/
(function () {
'use strict';
angular.module('BlurAdmin.theme')
.service('baUtil', baUtil);
/** @ngInject */
function baUtil() {
this.isDescendant = function(parent, child) {
var node = child.parentNode;
while (node != null) {
if (node == parent) {
return true;
}
node = node.parentNode;
}
return false;
};
}
})();

View File

@ -9,7 +9,7 @@
.run(themeRun);
/** @ngInject */
function themeRun($timeout, $rootScope, layoutSizes, layoutPaths, preloader, $q) {
function themeRun($timeout, $rootScope, layoutSizes, layoutPaths, preloader, $q, baSidebarService) {
$rootScope.$isMobile = (/android|webos|iphone|ipad|ipod|blackberry|windows phone/).test(navigator.userAgent.toLowerCase());
var whatToWait = [
@ -34,7 +34,7 @@
}
}, 7000);
$rootScope.$isMenuCollapsed = window.innerWidth <= layoutSizes.resWidthCollapseSidebar;
$rootScope.$baSidebarService = baSidebarService;
}
})();

View File

@ -26,10 +26,10 @@
</head>
<body ng-class="{'mobile' : $isMobile}">
<div class="body-bg"></div>
<main ng-if="$pageFinishedLoading" ng-class="{ 'menu-collapsed': $isMenuCollapsed }">
<main ng-if="$pageFinishedLoading" ng-class="{ 'menu-collapsed': $baSidebarService.isMenuCollapsed() }">
<sidebar></sidebar>
<page-top is-menu-collapsed="$isMenuCollapsed"></page-top>
<ba-sidebar></ba-sidebar>
<page-top></page-top>
<div class="al-main">
<div class="al-content">

View File

@ -46,6 +46,20 @@ $angle-right: "\f101";
}
}
.al-sidebar-list-item, .ba-sidebar-sublist-item {
&.ba-sidebar-item-expanded {
> .al-sidebar-list-link {
b {
transform: rotate(180deg);
}
}
> .al-sidebar-sublist {
display: block;
}
}
}
a.al-sidebar-list-link {
display: block;
height: 42px;
@ -83,6 +97,7 @@ a.al-sidebar-list-link {
padding: 0;
text-align: center;
color: #cccccc;
transition: transform 0.2s linear;
}
}