From 15f908541cf8e733770ea0f3a2e0b2f1722d7821 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sat, 2 Apr 2016 14:18:54 +0200 Subject: [PATCH] feat(sidenav): add gestures to sidenav to allow dragging the sidenav Fixes #1591 Fixes #35 --- .../sidenav/demoSidenavDragging/index.html | 73 +++++++++++++ .../sidenav/demoSidenavDragging/script.js | 27 +++++ src/components/sidenav/sidenav.js | 100 +++++++++++++++++- src/components/sidenav/sidenav.scss | 3 + 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/components/sidenav/demoSidenavDragging/index.html create mode 100644 src/components/sidenav/demoSidenavDragging/script.js diff --git a/src/components/sidenav/demoSidenavDragging/index.html b/src/components/sidenav/demoSidenavDragging/index.html new file mode 100644 index 00000000000..159aeefdf3f --- /dev/null +++ b/src/components/sidenav/demoSidenavDragging/index.html @@ -0,0 +1,73 @@ + +
+ +
+ + + + +

Drag Enabled

+
+ +

+ This sidenav is able to be dragged. Through the drag gesture, it's possible to close the sidenav. +

+ + Close Sidenav Left + +
+ +
+ + + +
+

+ You can simply close a sidenav by using a gesture. Simply drag the sidenav to close it. +

+

+ The right sidenav allows dragging. +

+

+ The left sidenav is disabled for dragging. +

+ +
+ + Toggle left + +
+ +
+ + Toggle right + +
+
+ +
+ +
+ + + + +

Drag Disabled

+
+ + + Close Sidenav Right + +

+ This sidenav disables the dragging gesture. It can be closed through the buttons. +

+
+ +
+ +
+ +
diff --git a/src/components/sidenav/demoSidenavDragging/script.js b/src/components/sidenav/demoSidenavDragging/script.js new file mode 100644 index 00000000000..b1b31d20f7f --- /dev/null +++ b/src/components/sidenav/demoSidenavDragging/script.js @@ -0,0 +1,27 @@ +angular + .module('sidenavDemo2', ['ngMaterial']) + .controller('AppCtrl', function ($scope, $timeout, $mdSidenav) { + $scope.toggleLeft = buildToggler('left'); + $scope.toggleRight = buildToggler('right'); + + $scope.isOpenRight = function(){ + return $mdSidenav('right').isOpen(); + }; + + function buildToggler(navID) { + return function() { + $mdSidenav(navID).toggle() + } + } + }) + .controller('LeftCtrl', function ($scope, $timeout, $mdSidenav) { + $scope.close = function () { + $mdSidenav('left').close(); + + }; + }) + .controller('RightCtrl', function ($scope, $timeout, $mdSidenav) { + $scope.close = function () { + $mdSidenav('right').close(); + }; + }); diff --git a/src/components/sidenav/sidenav.js b/src/components/sidenav/sidenav.js index c72fc4383df..1304cd51a59 100644 --- a/src/components/sidenav/sidenav.js +++ b/src/components/sidenav/sidenav.js @@ -197,6 +197,7 @@ function SidenavFocusDirective() { * @param {expression=} md-is-open A model bound to whether the sidenav is opened. * @param {boolean=} md-disable-backdrop When present in the markup, the sidenav will not show a backdrop. * @param {string=} md-component-id componentId to use with $mdSidenav service. + * @param {boolean=} md-disable-drag Disables the abbility to drag the sidenav * @param {expression=} md-is-locked-open When this expression evalutes to true, * the sidenav 'locks open': it falls into the content's flow instead * of appearing over it. This overrides the `md-is-open` attribute. @@ -209,7 +210,7 @@ function SidenavFocusDirective() { * - `` * - `` (locks open on small screens) */ -function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) { +function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $mdGesture, $parse, $log, $q, $document, $timeout) { return { restrict: 'E', scope: { @@ -265,6 +266,8 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, scope.$watch(isLocked, updateIsLocked); scope.$watch('isOpen', updateIsOpen); + // Enable dragging + if (!angular.isDefined(attr.mdDisableDrag) || !attr.mdDisableDrag) enableDragging(); // Publish special accessor for the Controller instance sidenavCtrl.$toggleOpen = toggleOpen; @@ -335,6 +338,101 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, } } + function enableDragging() { + $mdGesture.register(element, 'drag', { horizontal: true }); + + element + .on('$md.dragstart', onDragStart) + .on('$md.drag', onDrag) + .on('$md.dragend', onDragEnd); + + var style = getComputedStyle(element[0]); + var sidenavWidth = parseInt(style.width); + var isRightSidenav = element.hasClass('md-sidenav-right'); + var accelerationBound = 6; + + var dragCancelled = false; + var dragPercentage; + var lastOpenState; + var lastDistance = 0; + var isQuickDrag = false; + + function onDragStart() { + if (element.hasClass('md-locked-open')) { + dragCancelled = true; + } else { + lastOpenState = scope.isOpen; + element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms'); + } + } + + function onDrag(ev) { + if (dragCancelled) return; + + if (!isQuickDrag) { + var distance = lastDistance - ev.pointer.distanceX; + // When the current partial drag distance is bigger than the acceleration bound, then we can + // identify it as a quick drag. + isQuickDrag = isRightSidenav ? distance <= -accelerationBound : distance >= accelerationBound; + } else if (isRightSidenav && lastDistance > ev.pointer.distanceX || + !isRightSidenav && lastDistance < ev.pointer.distanceX) { + // When the users drags the sidenav backward, then we can reset the quick drag state. + isQuickDrag = false; + } + + dragPercentage = Math.round((ev.pointer.distanceX / sidenavWidth) * 100); + if (!isRightSidenav) dragPercentage = 0 - dragPercentage; + + if (dragPercentage > 100) dragPercentage = 100; + else if (dragPercentage < 0) dragPercentage = 0; + + element.css($mdConstant.CSS.TRANSFORM, 'translate3d(-' + (isRightSidenav ? 100 - dragPercentage : dragPercentage) + '%,0,0)'); + lastDistance = ev.pointer.distanceX; + } + + function onDragEnd() { + if (dragCancelled) { + dragCancelled = false; + return; + } + + var remainingPercentage = 100 - dragPercentage; + var animationTime = 4 * remainingPercentage; + var shouldClose = dragPercentage > 50 || isQuickDrag; + + // This validates the correct translate value. The invert is here required, because a right + // aligned sidenav will transition in the other direction. + var endTranslate = shouldClose ? isRightSidenav ? 0 : -100 : isRightSidenav ? -100 : 0; + + element.css($mdConstant.CSS.TRANSITION_DURATION, animationTime + "ms"); + element.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + endTranslate + '%,0,0)'); + + // Reset drag + lastDistance = 0; + isQuickDrag = false; + + // Invert shouldClose here, because we need to know if the sidenav should open. + $timeout(onAnimationDone, animationTime, true, !shouldClose); + } + + function onAnimationDone(isOpen) { + scope.isOpen = isOpen; + element.css($mdConstant.CSS.TRANSFORM, ''); + element.css($mdConstant.CSS.TRANSITION_DURATION, ''); + + if (isOpen) { + if (!lastOpenState && backdrop) { + $animate.enter(backdrop, element.parent()); + } + + element.removeClass('_md-closed'); + } else { + if (backdrop) $animate.leave(backdrop); + element.addClass('_md-closed'); + } + } + } + /** * Toggle the sideNav view and publish a promise to be resolved when * the view animation finishes. diff --git a/src/components/sidenav/sidenav.scss b/src/components/sidenav/sidenav.scss index 6f6948c10f5..9999083db9c 100644 --- a/src/components/sidenav/sidenav.scss +++ b/src/components/sidenav/sidenav.scss @@ -14,6 +14,9 @@ md-sidenav { overflow: auto; -webkit-overflow-scrolling: touch; + // This allows the most browsers to setup appropriate optimizations before the animation runs. + will-change: transform; + ul { list-style: none; }