mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(splitView): expose side menu on large viewport
Ability to keep a left menu exposed on larger viewports, such as a landscape tablet. Added the `expose-aside-menu` attribute directive.
This commit is contained in:
@@ -469,6 +469,12 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="{{ page.versionHref }}/api/directive/exposeAsideWhen/">
|
||||
expose-aside-when
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="{{ page.versionHref }}/api/directive/menuToggle/">
|
||||
menu-toggle
|
||||
|
||||
34
js/angular/controller/sideMenuController.js
vendored
34
js/angular/controller/sideMenuController.js
vendored
@@ -4,10 +4,11 @@ IonicModule
|
||||
'$attrs',
|
||||
'$ionicSideMenuDelegate',
|
||||
'$ionicPlatform',
|
||||
function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform) {
|
||||
'$document',
|
||||
function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $document) {
|
||||
var self = this;
|
||||
var rightShowing, leftShowing, isDragging;
|
||||
var startX, lastX, offsetX;
|
||||
var startX, lastX, offsetX, isAsideExposed;
|
||||
|
||||
self.$scope = $scope;
|
||||
|
||||
@@ -132,9 +133,9 @@ function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform) {
|
||||
}
|
||||
|
||||
if(percentage !== 0) {
|
||||
document.body.classList.add('menu-open');
|
||||
$document[0].body.classList.add('menu-open');
|
||||
} else {
|
||||
document.body.classList.remove('menu-open');
|
||||
$document[0].body.classList.remove('menu-open');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -253,8 +254,32 @@ function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform) {
|
||||
}
|
||||
};
|
||||
|
||||
self.isAsideExposed = function() {
|
||||
return !!isAsideExposed;
|
||||
};
|
||||
|
||||
self.exposeAside = function(shouldExposeAside) {
|
||||
isAsideExposed = shouldExposeAside;
|
||||
|
||||
// set the left marget width if it should be exposed
|
||||
// otherwise set false so there's no left margin
|
||||
self.content.setMarginLeft( isAsideExposed ? self.left.width : 0 );
|
||||
|
||||
self.$scope.$emit('$ionicExposeAside', isAsideExposed);
|
||||
};
|
||||
|
||||
self.activeAsideResizing = function(isResizing) {
|
||||
if(isResizing) {
|
||||
$document[0].body.classList.add('aside-resizing');
|
||||
} else {
|
||||
$document[0].body.classList.remove('aside-resizing');
|
||||
}
|
||||
};
|
||||
|
||||
// End a drag with the given event
|
||||
self._endDrag = function(e) {
|
||||
if(isAsideExposed) return;
|
||||
|
||||
if(isDragging) {
|
||||
self.snapToRest(e);
|
||||
}
|
||||
@@ -265,6 +290,7 @@ function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform) {
|
||||
|
||||
// Handle a drag event
|
||||
self._handleDrag = function(e) {
|
||||
if(isAsideExposed) return;
|
||||
|
||||
// If we don't have start coords, grab and store them
|
||||
if(!startX) {
|
||||
|
||||
77
js/angular/directive/exposeAsideWhen.js
vendored
Normal file
77
js/angular/directive/exposeAsideWhen.js
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name exposeAsideWhen
|
||||
* @module ionic
|
||||
* @restrict A
|
||||
* @parent ionic.directive:ionSideMenus
|
||||
*
|
||||
* @description
|
||||
* It is common for a tablet application to hide a menu when in portrait mode, but to show the
|
||||
* same menu on the left side when the tablet is in landscape mode. The `exposeAsideWhen` attribute
|
||||
* directive can be used to accomplish a similar interface.
|
||||
*
|
||||
* By default, side menus are hidden underneath its side menu content, and can be opened by either
|
||||
* swiping the content left or right, or toggling a button to show the side menu. However, by adding the
|
||||
* `exposeAsideWhen` attribute directive to an {@link ionic.directive:ionSideMenu} element directive,
|
||||
* a side menu can be given instructions on "when" the menu should be exposed (always viewable). For
|
||||
* example, the `expose-aside-when="large"` attribute will keep the side menu hidden when the viewport's
|
||||
* width is less than `768px`, but when the viewport's width is `768px` or greater, the menu will then
|
||||
* always be shown and can no longer be opened or closed like it could when it was hidden for smaller
|
||||
* viewports.
|
||||
*
|
||||
* Using `large` as the attribute's value is a shortcut value to `(min-width:768px)` since it is
|
||||
* the most common use-case. However, for added flexibility, any valid media query could be added
|
||||
* as the value, such as `(min-width:600px)` or even multiple queries such as
|
||||
* `(min-width:750px) and (max-width:1200px)`.
|
||||
|
||||
* @usage
|
||||
* ```html
|
||||
* <ion-side-menus>
|
||||
* <!-- Center content -->
|
||||
* <ion-side-menu-content>
|
||||
* </ion-side-menu-content>
|
||||
*
|
||||
* <!-- Left menu -->
|
||||
* <ion-side-menu expose-aside-when="large">
|
||||
* </ion-side-menu>
|
||||
* </ion-side-menus>
|
||||
* ```
|
||||
* For a complete side menu example, see the
|
||||
* {@link ionic.directive:ionSideMenus} documentation.
|
||||
*/
|
||||
IonicModule
|
||||
.directive('exposeAsideWhen', ['$window', function($window) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '^ionSideMenus',
|
||||
link: function($scope, $element, $attr, sideMenuCtrl) {
|
||||
|
||||
function checkAsideExpose() {
|
||||
var mq = $attr.exposeAsideWhen == 'large' ? '(min-width:768px)' : $attr.exposeAsideWhen;
|
||||
sideMenuCtrl.exposeAside( $window.matchMedia(mq).matches );
|
||||
sideMenuCtrl.activeAsideResizing(false);
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
sideMenuCtrl.activeAsideResizing(true);
|
||||
debouncedCheck();
|
||||
}
|
||||
|
||||
var debouncedCheck = ionic.debounce(function() {
|
||||
$scope.$apply(function(){
|
||||
checkAsideExpose();
|
||||
});
|
||||
}, 300, false);
|
||||
|
||||
checkAsideExpose();
|
||||
|
||||
ionic.on('resize', onResize, $window);
|
||||
|
||||
$scope.$on('$destroy', function(){
|
||||
ionic.off('resize', onResize, $window);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
26
js/angular/directive/sideMenuContent.js
vendored
26
js/angular/directive/sideMenuContent.js
vendored
@@ -30,7 +30,8 @@ IonicModule
|
||||
.directive('ionSideMenuContent', [
|
||||
'$timeout',
|
||||
'$ionicGesture',
|
||||
function($timeout, $ionicGesture) {
|
||||
'$window',
|
||||
function($timeout, $ionicGesture, $window) {
|
||||
|
||||
return {
|
||||
restrict: 'EA', //DEPRECATED 'A'
|
||||
@@ -126,7 +127,7 @@ function($timeout, $ionicGesture) {
|
||||
}
|
||||
}
|
||||
|
||||
sideMenuCtrl.setContent({
|
||||
var content = {
|
||||
element: element[0],
|
||||
onDrag: function(e) {},
|
||||
endDrag: function(e) {},
|
||||
@@ -134,11 +135,23 @@ function($timeout, $ionicGesture) {
|
||||
return $scope.sideMenuContentTranslateX || 0;
|
||||
},
|
||||
setTranslateX: ionic.animationFrameThrottle(function(amount) {
|
||||
$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amount + 'px, 0, 0)';
|
||||
var xTransform = content.offsetX + amount;
|
||||
$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + xTransform + 'px,0,0)';
|
||||
$timeout(function() {
|
||||
$scope.sideMenuContentTranslateX = amount;
|
||||
});
|
||||
}),
|
||||
setMarginLeft: ionic.animationFrameThrottle(function(amount) {
|
||||
if(amount) {
|
||||
$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amount + 'px,0,0)';
|
||||
$element[0].style.width = ($window.innerWidth - amount) + 'px';
|
||||
content.offsetX = amount;
|
||||
} else {
|
||||
$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
|
||||
$element[0].style.width = '';
|
||||
content.offsetX = 0;
|
||||
}
|
||||
}),
|
||||
enableAnimation: function() {
|
||||
$scope.animationEnabled = true;
|
||||
$element[0].classList.add('menu-animated');
|
||||
@@ -146,8 +159,11 @@ function($timeout, $ionicGesture) {
|
||||
disableAnimation: function() {
|
||||
$scope.animationEnabled = false;
|
||||
$element[0].classList.remove('menu-animated');
|
||||
}
|
||||
});
|
||||
},
|
||||
offsetX: 0
|
||||
};
|
||||
|
||||
sideMenuCtrl.setContent(content);
|
||||
|
||||
// add gesture handlers
|
||||
var dragRightGesture = $ionicGesture.on('dragright', onDragX, $element);
|
||||
|
||||
32
js/angular/directive/sideMenus.js
vendored
32
js/angular/directive/sideMenus.js
vendored
@@ -17,6 +17,12 @@ IonicModule
|
||||
* links and buttons within `ion-side-menu` content, so that when the element is
|
||||
* clicked then the opened side menu will automatically close.
|
||||
*
|
||||
* By default, side menus are hidden underneath its side menu content, and can be opened by
|
||||
* either swiping the content left or right, or toggling a button to show the side menu. However,
|
||||
* by adding the {@link ionic.directive:exposeAsideWhen} attribute directive to an
|
||||
* {@link ionic.directive:ionSideMenu} element directive, a side menu can be given instructions
|
||||
* on "when" the menu should be exposed (always viewable).
|
||||
*
|
||||
* 
|
||||
*
|
||||
* For more information on side menus, check out:
|
||||
@@ -24,6 +30,7 @@ IonicModule
|
||||
* - {@link ionic.directive:ionSideMenuContent}
|
||||
* - {@link ionic.directive:ionSideMenu}
|
||||
* - {@link ionic.directive:menuClose}
|
||||
* - {@link ionic.directive:exposeAsideWhen}
|
||||
*
|
||||
* @usage
|
||||
* To use side menus, add an `<ion-side-menus>` parent element,
|
||||
@@ -58,18 +65,35 @@ IonicModule
|
||||
*
|
||||
*/
|
||||
.directive('ionSideMenus', ['$document', function($document) {
|
||||
|
||||
var ASIDE_OPEN_CSS = 'aside-open';
|
||||
|
||||
return {
|
||||
restrict: 'ECA',
|
||||
controller: '$ionicSideMenus',
|
||||
compile: function(element, attr) {
|
||||
attr.$set('class', (attr['class'] || '') + ' view');
|
||||
|
||||
return function($scope) {
|
||||
$scope.$on('$destroy', function(){
|
||||
$document[0].body.classList.remove('menu-open');
|
||||
return { pre: prelink };
|
||||
function prelink($scope) {
|
||||
var bodyClassList = $document[0].body.classList;
|
||||
|
||||
$scope.$on('$ionicExposeAside', function(evt, isAsideExposed){
|
||||
if(!$scope.$exposeAside) $scope.$exposeAside = {};
|
||||
$scope.$exposeAside.active = isAsideExposed;
|
||||
if(isAsideExposed) {
|
||||
bodyClassList.add(ASIDE_OPEN_CSS);
|
||||
} else {
|
||||
bodyClassList.remove(ASIDE_OPEN_CSS);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
$scope.$on('$destroy', function(){
|
||||
bodyClassList.remove('menu-open');
|
||||
bodyClassList.remove(ASIDE_OPEN_CSS);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -55,6 +55,10 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.aside-open.aside-resizing .menu-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-animated {
|
||||
@include transition-transform($menu-animation-speed ease);
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
|
||||
/**
|
||||
* Split Pane
|
||||
* --------------------------------------------------
|
||||
*/
|
||||
|
||||
.split-pane {
|
||||
@include display-flex();
|
||||
@include align-items(stretch);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.split-pane-menu {
|
||||
@include flex(0, 0, $split-pane-menu-width);
|
||||
|
||||
overflow-y: auto;
|
||||
width: $split-pane-menu-width;
|
||||
height: 100%;
|
||||
border-right: 1px solid $split-pane-menu-border-color;
|
||||
|
||||
@media all and (max-width: 568px) {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.split-pane-content {
|
||||
@include flex(1, 0, auto);
|
||||
}
|
||||
@@ -543,9 +543,6 @@ $menu-animation-speed: 200ms !default;
|
||||
|
||||
$menu-side-shadow: -1px 0px 2px rgba(0, 0, 0, 0.2), 1px 0px 2px rgba(0,0,0,0.2) !default;
|
||||
|
||||
$split-pane-menu-width: 320px !default;
|
||||
$split-pane-menu-border-color: #eee !default;
|
||||
|
||||
|
||||
// Modals
|
||||
// -------------------------------
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"list",
|
||||
"badge",
|
||||
"slide-box",
|
||||
"split-pane",
|
||||
|
||||
// Forms
|
||||
"form",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<ion-side-menu-content edge-drag-threshold="true" drag-content="!$root.disableDrag">
|
||||
<header class="bar bar-header bar-positive">
|
||||
<button class="button button-icon ion-navicon" ng-click="toggleLeft()"></button>
|
||||
<button class="button button-icon ion-navicon" ng-click="toggleLeft()" ng-hide="$exposeAside.active"></button>
|
||||
<h1 class="title">Slide me</h1>
|
||||
<button class="button button-icon ion-navicon" ng-click="toggleRight()"></button>
|
||||
</header>
|
||||
@@ -32,7 +32,7 @@
|
||||
</ion-content>
|
||||
</ion-side-menu-content>
|
||||
|
||||
<ion-side-menu side="left" width="$root.menuWidth || 270" ng-controller="LeftCtrl">
|
||||
<ion-side-menu side="left" width="$root.menuWidth || 275" ng-controller="LeftCtrl" expose-aside-when="(min-width:750px) and (max-width:1200px)">
|
||||
<header class="bar bar-header bar-assertive">
|
||||
<h1 class="title">Left</h1>
|
||||
</header>
|
||||
|
||||
@@ -24,6 +24,55 @@ describe('Ionic Angular Side Menu', function() {
|
||||
expect(deregisterSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set $exposeAside.active', inject(function($compile, $rootScope) {
|
||||
var el = $compile('<ion-side-menus><ion-side-menu></><ion-side-menu-content></ion-side-menu-content></ion-side-menus>')($rootScope.$new());
|
||||
$rootScope.$apply();
|
||||
var sideMenuController = el.controller('ionSideMenus');
|
||||
expect(sideMenuController.isAsideExposed()).toBe(false);
|
||||
expect(el.scope().$exposeAside).toBeUndefined();
|
||||
|
||||
sideMenuController.exposeAside(true);
|
||||
expect(el.scope().$exposeAside.active).toEqual(true);
|
||||
expect(sideMenuController.isAsideExposed()).toBe(true);
|
||||
|
||||
sideMenuController.exposeAside(false);
|
||||
expect(el.scope().$exposeAside.active).toEqual(false);
|
||||
expect(sideMenuController.isAsideExposed()).toBe(false);
|
||||
}));
|
||||
|
||||
it('should emit $ionicexposeAside', inject(function($compile, $rootScope) {
|
||||
var el = $compile('<ion-side-menus><ion-side-menu></><ion-side-menu-content></ion-side-menu-content></ion-side-menus>')($rootScope.$new());
|
||||
$rootScope.$apply();
|
||||
var sideMenuController = el.controller('ionSideMenus');
|
||||
|
||||
spyOn(el.scope(), "$emit")
|
||||
sideMenuController.exposeAside(true);
|
||||
expect(el.scope().$emit).toHaveBeenCalledWith("$ionicExposeAside", true);
|
||||
|
||||
sideMenuController.exposeAside(false);
|
||||
expect(el.scope().$emit).toHaveBeenCalledWith("$ionicExposeAside", false);
|
||||
}));
|
||||
|
||||
it('should set exposed menu', inject(function($compile, $rootScope) {
|
||||
var el = $compile('<ion-side-menus><ion-side-menu></><ion-side-menu-content></ion-side-menu-content></ion-side-menus>')($rootScope.$new());
|
||||
$rootScope.$apply();
|
||||
var sideMenuController = el.controller('ionSideMenus');
|
||||
var content = sideMenuController.content;
|
||||
expect(content.offsetX).toEqual(0);
|
||||
expect(content.getTranslateX()).toEqual(0);
|
||||
expect(content.element.style.width).toEqual('');
|
||||
sideMenuController.exposeAside(true);
|
||||
expect(content.offsetX).toEqual(275);
|
||||
expect(content.getTranslateX()).toEqual(0);
|
||||
expect(content.element.getAttribute('style').indexOf('translate3d(275px, 0px, 0px)') > -1).toEqual(true);
|
||||
expect(content.element.style.width).toNotEqual('');
|
||||
sideMenuController.exposeAside(false);
|
||||
expect(content.element.getAttribute('style').indexOf('translate3d(0px, 0px, 0px)') > -1).toEqual(true);
|
||||
expect(content.getTranslateX()).toEqual(0);
|
||||
expect(content.offsetX).toEqual(0);
|
||||
expect(content.element.style.width).toEqual('');
|
||||
}));
|
||||
|
||||
it('should canDragContent', inject(function($compile, $rootScope) {
|
||||
var el = $compile('<ion-side-menus><div ion-side-menu-content></div></ion-side-menus>')($rootScope.$new());
|
||||
$rootScope.$apply();
|
||||
|
||||
Reference in New Issue
Block a user