/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v0.10.0 */ goog.provide('ng.material.components.sidenav'); goog.require('ng.material.components.backdrop'); goog.require('ng.material.core'); /** * @ngdoc module * @name material.components.sidenav * * @description * A Sidenav QP component. */ angular.module('material.components.sidenav', [ 'material.core', 'material.components.backdrop' ]) .factory('$mdSidenav', SidenavService ) .directive('mdSidenav', SidenavDirective) .directive('mdSidenavFocus', SidenavFocusDirective) .controller('$mdSidenavController', SidenavController); /** * @private * @ngdoc service * @name $mdSidenav * @module material.components.sidenav * * @description * `$mdSidenav` makes it easy to interact with multiple sidenavs * in an app. * * @usage * * // Async lookup for sidenav instance; will resolve when the instance is available * $mdSidenav(componentId).then(function(instance) { * $log.debug( componentId + "is now ready" ); * }); * // Async toggle the given sidenav; * // when instance is known ready and lazy lookup is not needed. * $mdSidenav(componentId) * .toggle() * .then(function(){ * $log.debug('toggled'); * }); * // Async open the given sidenav * $mdSidenav(componentId) * .open() * .then(function(){ * $log.debug('opened'); * }); * // Async close the given sidenav * $mdSidenav(componentId) * .close() * .then(function(){ * $log.debug('closed'); * }); * // Sync check to see if the specified sidenav is set to be open * $mdSidenav(componentId).isOpen(); * // Sync check to whether given sidenav is locked open * // If this is true, the sidenav will be open regardless of close() * $mdSidenav(componentId).isLockedOpen(); * */ function SidenavService($mdComponentRegistry, $q) { return function(handle) { // Lookup the controller instance for the specified sidNav instance var self; var errorMsg = "SideNav '" + handle + "' is not available!"; var instance = $mdComponentRegistry.get(handle); if(!instance) { $mdComponentRegistry.notFoundError(handle); } return self = { // ----------------- // Sync methods // ----------------- isOpen: function() { return instance && instance.isOpen(); }, isLockedOpen: function() { return instance && instance.isLockedOpen(); }, // ----------------- // Async methods // ----------------- toggle: function() { return instance ? instance.toggle() : $q.reject(errorMsg); }, open: function() { return instance ? instance.open() : $q.reject(errorMsg); }, close: function() { return instance ? instance.close() : $q.reject(errorMsg); }, then : function( callbackFn ) { var promise = instance ? $q.when(instance) : waitForInstance(); return promise.then( callbackFn || angular.noop ); } }; /** * Deferred lookup of component instance using $component registry */ function waitForInstance() { return $mdComponentRegistry .when(handle) .then(function( it ){ instance = it; return it; }); } }; } SidenavService.$inject = ["$mdComponentRegistry", "$q"]; /** * @ngdoc directive * @name mdSidenavFocus * @module material.components.sidenav * * @restrict A * * @description * `$mdSidenavFocus` provides a way to specify the focused element when a sidenav opens. * This is completely optional, as the sidenav itself is focused by default. * * @usage * * *
* * * * *
*
*
**/ function SidenavFocusDirective() { return { restrict: 'A', require: '^mdSidenav', link: function(scope, element, attr, sidenavCtrl) { sidenavCtrl.focusElement(element); } }; } /** * @ngdoc directive * @name mdSidenav * @module material.components.sidenav * @restrict E * * @description * * A Sidenav component that can be opened and closed programatically. * * By default, upon opening it will slide out on top of the main content area. * * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default. * It can be overridden with the `md-sidenav-focus` directive on the child element you want focused. * * @usage * *
* * Left Nav! * * * * Center Content * * Open Left Menu * * * * *
* * * * *
*
*
*
* * * var app = angular.module('myApp', ['ngMaterial']); * app.controller('MyController', function($scope, $mdSidenav) { * $scope.openLeftMenu = function() { * $mdSidenav('left').toggle(); * }; * }); * * * @param {expression=} md-is-open A model bound to whether the sidenav is opened. * @param {string=} md-component-id componentId to use with $mdSidenav service. * @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 `is-open` attribute. * * The $mdMedia() service is exposed to the is-locked-open attribute, which * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets. * Examples: * * - `` * - `` * - `` (locks open on small screens) */ function SidenavDirective($timeout, $animate, $parse, $log, $mdMedia, $mdConstant, $compile, $mdTheming, $q, $document) { return { restrict: 'E', scope: { isOpen: '=?mdIsOpen' }, controller: '$mdSidenavController', compile: function(element) { element.addClass('md-closed'); element.attr('tabIndex', '-1'); return postLink; } }; /** * Directive Post Link function... */ function postLink(scope, element, attr, sidenavCtrl) { var lastParentOverFlow; var triggeringElement = null; var promise = $q.when(true); var isLockedOpenParsed = $parse(attr.mdIsLockedOpen); var isLocked = function() { return isLockedOpenParsed(scope.$parent, { $media: function(arg) { $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead."); return $mdMedia(arg); }, $mdMedia: $mdMedia }); }; var backdrop = $compile( '' )(scope); element.on('$destroy', sidenavCtrl.destroy); $mdTheming.inherit(backdrop, element); scope.$watch(isLocked, updateIsLocked); scope.$watch('isOpen', updateIsOpen); // Publish special accessor for the Controller instance sidenavCtrl.$toggleOpen = toggleOpen; sidenavCtrl.focusElement( sidenavCtrl.focusElement() || element ); /** * Toggle the DOM classes to indicate `locked` * @param isLocked */ function updateIsLocked(isLocked, oldValue) { scope.isLockedOpen = isLocked; if (isLocked === oldValue) { element.toggleClass('md-locked-open', !!isLocked); } else { $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open'); } backdrop.toggleClass('md-locked-open', !!isLocked); } /** * Toggle the SideNav view and attach/detach listeners * @param isOpen */ function updateIsOpen(isOpen) { var parent = element.parent(); parent[isOpen ? 'on' : 'off']('keydown', onKeyDown); backdrop[isOpen ? 'on' : 'off']('click', close); if ( isOpen ) { // Capture upon opening.. triggeringElement = $document[0].activeElement; } var focusEl = sidenavCtrl.focusElement(); disableParentScroll(isOpen); return promise = $q.all([ isOpen ? $animate.enter(backdrop, parent) : $animate.leave(backdrop), $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed') ]) .then(function() { // Perform focus when animations are ALL done... if (scope.isOpen) { focusEl && focusEl.focus(); } }); } /** * Prevent parent scrolling (when the SideNav is open) */ function disableParentScroll(disabled) { var parent = element.parent(); if ( disabled ) { lastParentOverFlow = parent.css('overflow'); parent.css('overflow', 'hidden'); } else if (angular.isDefined(lastParentOverFlow)) { parent.css('overflow', lastParentOverFlow); lastParentOverFlow = undefined; } } /** * Toggle the sideNav view and publish a promise to be resolved when * the view animation finishes. * * @param isOpen * @returns {*} */ function toggleOpen( isOpen ) { if (scope.isOpen == isOpen ) { return $q.when(true); } else { var deferred = $q.defer(); // Toggle value to force an async `updateIsOpen()` to run scope.isOpen = isOpen; $timeout(function() { // When the current `updateIsOpen()` animation finishes promise.then(function(result) { if ( !scope.isOpen ) { // reset focus to originating element (if available) upon close triggeringElement && triggeringElement.focus(); triggeringElement = null; } deferred.resolve(result); }); },0,false); return deferred.promise; } } /** * Auto-close sideNav when the `escape` key is pressed. * @param evt */ function onKeyDown(ev) { var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE); return isEscape ? close(ev) : $q.when(true); } /** * With backdrop `clicks` or `escape` key-press, immediately * apply the CSS close transition... Then notify the controller * to close() and perform its own actions. */ function close(ev) { ev.preventDefault(); ev.stopPropagation(); return sidenavCtrl.close(); } } } SidenavDirective.$inject = ["$timeout", "$animate", "$parse", "$log", "$mdMedia", "$mdConstant", "$compile", "$mdTheming", "$q", "$document"]; /* * @private * @ngdoc controller * @name SidenavController * @module material.components.sidenav * */ function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) { var self = this, focusElement; // Use Default internal method until overridden by directive postLink // Synchronous getters self.isOpen = function() { return !!$scope.isOpen; }; self.isLockedOpen = function() { return !!$scope.isLockedOpen; }; // Async actions self.open = function() { return self.$toggleOpen( true ); }; self.close = function() { return self.$toggleOpen( false ); }; self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); }; self.focusElement = function(el) { if ( angular.isDefined(el) ) { focusElement = el; } return focusElement; }; self.$toggleOpen = function() { return $q.when($scope.isOpen); }; self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId); } SidenavController.$inject = ["$scope", "$element", "$attrs", "$mdComponentRegistry", "$q"]; ng.material.components.sidenav = angular.module("material.components.sidenav");