2  * Angular Material Design
 
   3  * https://github.com/angular/material
 
   7 (function( window, angular, undefined ){
 
  12  * @name material.components.sidenav
 
  15  * A Sidenav QP component.
 
  17 SidenavService['$inject'] = ["$mdComponentRegistry", "$mdUtil", "$q", "$log"];
 
  18 SidenavDirective['$inject'] = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$mdInteraction", "$animate", "$compile", "$parse", "$log", "$q", "$document", "$window", "$$rAF"];
 
  19 SidenavController['$inject'] = ["$scope", "$attrs", "$mdComponentRegistry", "$q", "$interpolate"];
 
  21   .module('material.components.sidenav', [
 
  23     'material.components.backdrop'
 
  25   .factory('$mdSidenav', SidenavService )
 
  26   .directive('mdSidenav', SidenavDirective)
 
  27   .directive('mdSidenavFocus', SidenavFocusDirective)
 
  28   .controller('$mdSidenavController', SidenavController);
 
  34  * @module material.components.sidenav
 
  37  * `$mdSidenav` makes it easy to interact with multiple sidenavs
 
  38  * in an app. When looking up a sidenav instance, you can either look
 
  39  * it up synchronously or wait for it to be initializied asynchronously.
 
  40  * This is done by passing the second argument to `$mdSidenav`.
 
  44  * // Async lookup for sidenav instance; will resolve when the instance is available
 
  45  * $mdSidenav(componentId, true).then(function(instance) {
 
  46  *   $log.debug( componentId + "is now ready" );
 
  48  * // Sync lookup for sidenav instance; this will resolve immediately.
 
  49  * $mdSidenav(componentId).then(function(instance) {
 
  50  *   $log.debug( componentId + "is now ready" );
 
  52  * // Async toggle the given sidenav;
 
  53  * // when instance is known ready and lazy lookup is not needed.
 
  54  * $mdSidenav(componentId)
 
  57  *      $log.debug('toggled');
 
  59  * // Async open the given sidenav
 
  60  * $mdSidenav(componentId)
 
  63  *      $log.debug('opened');
 
  65  * // Async close the given sidenav
 
  66  * $mdSidenav(componentId)
 
  69  *      $log.debug('closed');
 
  71  * // Sync check to see if the specified sidenav is set to be open
 
  72  * $mdSidenav(componentId).isOpen();
 
  73  * // Sync check to whether given sidenav is locked open
 
  74  * // If this is true, the sidenav will be open regardless of close()
 
  75  * $mdSidenav(componentId).isLockedOpen();
 
  76  * // On close callback to handle close, backdrop click or escape key pressed
 
  77  * // Callback happens BEFORE the close action occurs.
 
  78  * $mdSidenav(componentId).onClose(function () {
 
  79  *   $log.debug('closing');
 
  83 function SidenavService($mdComponentRegistry, $mdUtil, $q, $log) {
 
  84   var errorMsg = "SideNav '{0}' is not available! Did you use md-component-id='{0}'?";
 
  86         find    : findInstance,     //  sync  - returns proxy API
 
  87         waitFor : waitForInstance   //  async - returns promise
 
  91    * Service API that supports three (3) usages:
 
  92    *   $mdSidenav().find("left")                       // sync (must already exist) or returns undefined
 
  93    *   $mdSidenav("left").toggle();                    // sync (must already exist) or returns reject promise;
 
  94    *   $mdSidenav("left",true).then( function(left){   // async returns instance when available
 
  98   return function(handle, enableWait) {
 
  99     if ( angular.isUndefined(handle) ) return service;
 
 101     var shouldWait = enableWait === true;
 
 102     var instance = service.find(handle, shouldWait);
 
 103     return  !instance && shouldWait ? service.waitFor(handle) :
 
 104             !instance && angular.isUndefined(enableWait) ? addLegacyAPI(service, handle) : instance;
 
 108    * For failed instance/handle lookups, older-clients expect an response object with noops
 
 109    * that include `rejected promise APIs`
 
 111   function addLegacyAPI(service, handle) {
 
 112       var falseFn  = function() { return false; };
 
 113       var rejectFn = function() {
 
 114             return $q.when($mdUtil.supplant(errorMsg, [handle || ""]));
 
 117       return angular.extend({
 
 118         isLockedOpen : falseFn,
 
 123         onClose      : angular.noop,
 
 124         then : function(callback) {
 
 125           return waitForInstance(handle)
 
 126             .then(callback || angular.noop);
 
 131      * Synchronously lookup the controller instance for the specified sidNav instance which has been
 
 132      * registered with the markup `md-component-id`
 
 134     function findInstance(handle, shouldWait) {
 
 135       var instance = $mdComponentRegistry.get(handle);
 
 137       if (!instance && !shouldWait) {
 
 139         // Report missing instance
 
 140         $log.error( $mdUtil.supplant(errorMsg, [handle || ""]) );
 
 142         // The component has not registered itself... most like NOT yet created
 
 143         // return null to indicate that the Sidenav is not in the DOM
 
 150      * Asynchronously wait for the component instantiation,
 
 151      * Deferred lookup of component instance using $component registry
 
 153     function waitForInstance(handle) {
 
 154       return $mdComponentRegistry.when(handle).catch($log.error);
 
 159  * @name mdSidenavFocus
 
 160  * @module material.components.sidenav
 
 165  * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens.
 
 166  * This is completely optional, as the sidenav itself is focused by default.
 
 172  *     <md-input-container>
 
 173  *       <label for="testInput">Label</label>
 
 174  *       <input id="testInput" type="text" md-sidenav-focus>
 
 175  *     </md-input-container>
 
 180 function SidenavFocusDirective() {
 
 183     require: '^mdSidenav',
 
 184     link: function(scope, element, attr, sidenavCtrl) {
 
 185       // @see $mdUtil.findFocusTarget(...)
 
 192  * @module material.components.sidenav
 
 197  * A Sidenav component that can be opened and closed programatically.
 
 199  * By default, upon opening it will slide out on top of the main content area.
 
 201  * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
 
 202  * It can be overridden with the `md-autofocus` directive on the child element you want focused.
 
 206  * <div layout="row" ng-controller="MyController">
 
 207  *   <md-sidenav md-component-id="left" class="md-sidenav-left">
 
 213  *     <md-button ng-click="openLeftMenu()">
 
 218  *   <md-sidenav md-component-id="right"
 
 219  *     md-is-locked-open="$mdMedia('min-width: 333px')"
 
 220  *     class="md-sidenav-right">
 
 222  *       <md-input-container>
 
 223  *         <label for="testInput">Test input</label>
 
 224  *         <input id="testInput" type="text"
 
 225  *                ng-model="data" md-autofocus>
 
 226  *       </md-input-container>
 
 233  * var app = angular.module('myApp', ['ngMaterial']);
 
 234  * app.controller('MyController', function($scope, $mdSidenav) {
 
 235  *   $scope.openLeftMenu = function() {
 
 236  *     $mdSidenav('left').toggle();
 
 241  * @param {expression=} md-is-open A model bound to whether the sidenav is opened.
 
 242  * @param {boolean=} md-disable-backdrop When present in the markup, the sidenav will not show a backdrop.
 
 243  * @param {string=} md-component-id componentId to use with $mdSidenav service.
 
 244  * @param {expression=} md-is-locked-open When this expression evaluates to true,
 
 245  * the sidenav 'locks open': it falls into the content's flow instead
 
 246  * of appearing over it. This overrides the `md-is-open` attribute.
 
 247  * @param {string=} md-disable-scroll-target Selector, pointing to an element, whose scrolling will
 
 248  * be disabled when the sidenav is opened. By default this is the sidenav's direct parent.
 
 250 * The $mdMedia() service is exposed to the is-locked-open attribute, which
 
 251  * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets.
 
 254  *   - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
 
 255  *   - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
 
 256  *   - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
 
 258 function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate,
 
 259                           $compile, $parse, $log, $q, $document, $window, $$rAF) {
 
 265     controller: '$mdSidenavController',
 
 266     compile: function(element) {
 
 267       element.addClass('md-closed').attr('tabIndex', '-1');
 
 273    * Directive Post Link function...
 
 275   function postLink(scope, element, attr, sidenavCtrl) {
 
 276     var lastParentOverFlow;
 
 278     var disableScrollTarget = null;
 
 279     var triggeringInteractionType;
 
 280     var triggeringElement = null;
 
 281     var previousContainerStyles;
 
 282     var promise = $q.when(true);
 
 283     var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
 
 284     var ngWindow = angular.element($window);
 
 285     var isLocked = function() {
 
 286       return isLockedOpenParsed(scope.$parent, {
 
 287         $media: function(arg) {
 
 288           $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead.");
 
 289           return $mdMedia(arg);
 
 295     if (attr.mdDisableScrollTarget) {
 
 296       disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget);
 
 298       if (disableScrollTarget) {
 
 299         disableScrollTarget = angular.element(disableScrollTarget);
 
 301         $log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' +
 
 302           'selector "{selector}". Falling back to parent.', { selector: attr.mdDisableScrollTarget }));
 
 306     if (!disableScrollTarget) {
 
 307       disableScrollTarget = element.parent();
 
 310     // Only create the backdrop if the backdrop isn't disabled.
 
 311     if (!attr.hasOwnProperty('mdDisableBackdrop')) {
 
 312       backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
 
 315     element.addClass('_md');     // private md component indicator for styling
 
 318     // The backdrop should inherit the sidenavs theme,
 
 319     // because the backdrop will take its parent theme by default.
 
 320     if ( backdrop ) $mdTheming.inherit(backdrop, element);
 
 322     element.on('$destroy', function() {
 
 323       backdrop && backdrop.remove();
 
 324       sidenavCtrl.destroy();
 
 327     scope.$on('$destroy', function(){
 
 328       backdrop && backdrop.remove();
 
 331     scope.$watch(isLocked, updateIsLocked);
 
 332     scope.$watch('isOpen', updateIsOpen);
 
 335     // Publish special accessor for the Controller instance
 
 336     sidenavCtrl.$toggleOpen = toggleOpen;
 
 339      * Toggle the DOM classes to indicate `locked`
 
 342     function updateIsLocked(isLocked, oldValue) {
 
 343       scope.isLockedOpen = isLocked;
 
 344       if (isLocked === oldValue) {
 
 345         element.toggleClass('md-locked-open', !!isLocked);
 
 347         $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
 
 350         backdrop.toggleClass('md-locked-open', !!isLocked);
 
 355      * Toggle the SideNav view and attach/detach listeners
 
 358     function updateIsOpen(isOpen) {
 
 359       // Support deprecated md-sidenav-focus attribute as fallback
 
 360       var focusEl = $mdUtil.findFocusTarget(element) || $mdUtil.findFocusTarget(element,'[md-sidenav-focus]') || element;
 
 361       var parent = element.parent();
 
 363       parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
 
 364       if (backdrop) backdrop[isOpen ? 'on' : 'off']('click', close);
 
 366       var restorePositioning = updateContainerPositions(parent, isOpen);
 
 369         // Capture upon opening..
 
 370         triggeringElement = $document[0].activeElement;
 
 371         triggeringInteractionType = $mdInteraction.getLastInteractionType();
 
 374       disableParentScroll(isOpen);
 
 376       return promise = $q.all([
 
 377         isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
 
 378                              $animate.leave(backdrop) : $q.when(true),
 
 379         $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
 
 381         // Perform focus when animations are ALL done...
 
 384             // Notifies child components that the sidenav was opened. Should wait
 
 385             // a frame in order to allow for the element height to be computed.
 
 386             ngWindow.triggerHandler('resize');
 
 389           focusEl && focusEl.focus();
 
 392         // Restores the positioning on the sidenav and backdrop.
 
 393         restorePositioning && restorePositioning();
 
 397     function updateContainerPositions(parent, willOpen) {
 
 398       var drawerEl = element[0];
 
 399       var scrollTop = parent[0].scrollTop;
 
 401       if (willOpen && scrollTop) {
 
 402         previousContainerStyles = {
 
 403           top: drawerEl.style.top,
 
 404           bottom: drawerEl.style.bottom,
 
 405           height: drawerEl.style.height
 
 408         // When the parent is scrolled down, then we want to be able to show the sidenav at the current scroll
 
 409         // position. We're moving the sidenav down to the correct scroll position and apply the height of the
 
 410         // parent, to increase the performance. Using 100% as height, will impact the performance heavily.
 
 411         var positionStyle = {
 
 412           top: scrollTop + 'px',
 
 414           height: parent[0].clientHeight + 'px'
 
 417         // Apply the new position styles to the sidenav and backdrop.
 
 418         element.css(positionStyle);
 
 419         backdrop.css(positionStyle);
 
 422       // When the sidenav is closing and we have previous defined container styles,
 
 423       // then we return a restore function, which resets the sidenav and backdrop.
 
 424       if (!willOpen && previousContainerStyles) {
 
 426           drawerEl.style.top = previousContainerStyles.top;
 
 427           drawerEl.style.bottom = previousContainerStyles.bottom;
 
 428           drawerEl.style.height = previousContainerStyles.height;
 
 430           backdrop[0].style.top = null;
 
 431           backdrop[0].style.bottom = null;
 
 432           backdrop[0].style.height = null;
 
 434           previousContainerStyles = null;
 
 440      * Prevent parent scrolling (when the SideNav is open)
 
 442     function disableParentScroll(disabled) {
 
 443       if ( disabled && !lastParentOverFlow ) {
 
 444         lastParentOverFlow = disableScrollTarget.css('overflow');
 
 445         disableScrollTarget.css('overflow', 'hidden');
 
 446       } else if (angular.isDefined(lastParentOverFlow)) {
 
 447         disableScrollTarget.css('overflow', lastParentOverFlow);
 
 448         lastParentOverFlow = undefined;
 
 453      * Toggle the sideNav view and publish a promise to be resolved when
 
 454      * the view animation finishes.
 
 459     function toggleOpen( isOpen ) {
 
 460       if (scope.isOpen == isOpen ) {
 
 462         return $q.when(true);
 
 465         if (scope.isOpen && sidenavCtrl.onCloseCb) sidenavCtrl.onCloseCb();
 
 467         return $q(function(resolve){
 
 468           // Toggle value to force an async `updateIsOpen()` to run
 
 469           scope.isOpen = isOpen;
 
 471           $mdUtil.nextTick(function() {
 
 472             // When the current `updateIsOpen()` animation finishes
 
 473             promise.then(function(result) {
 
 475               if ( !scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') {
 
 476                 // reset focus to originating element (if available) upon close
 
 477                 triggeringElement.focus();
 
 478                 triggeringElement = null;
 
 491      * Auto-close sideNav when the `escape` key is pressed.
 
 494     function onKeyDown(ev) {
 
 495       var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
 
 496       return isEscape ? close(ev) : $q.when(true);
 
 500      * With backdrop `clicks` or `escape` key-press, immediately
 
 501      * apply the CSS close transition... Then notify the controller
 
 502      * to close() and perform its own actions.
 
 507       return sidenavCtrl.close();
 
 516  * @name SidenavController
 
 517  * @module material.components.sidenav
 
 519 function SidenavController($scope, $attrs, $mdComponentRegistry, $q, $interpolate) {
 
 523   // Use Default internal method until overridden by directive postLink
 
 525   // Synchronous getters
 
 526   self.isOpen = function() { return !!$scope.isOpen; };
 
 527   self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
 
 529   // Synchronous setters
 
 530   self.onClose = function (callback) {
 
 531     self.onCloseCb = callback;
 
 536   self.open   = function() { return self.$toggleOpen( true );  };
 
 537   self.close  = function() { return self.$toggleOpen( false ); };
 
 538   self.toggle = function() { return self.$toggleOpen( !$scope.isOpen );  };
 
 539   self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); };
 
 541   // Evaluate the component id.
 
 542   var rawId = $attrs.mdComponentId;
 
 543   var hasDataBinding = rawId && rawId.indexOf($interpolate.startSymbol()) > -1;
 
 544   var componentId = hasDataBinding ? $interpolate(rawId)($scope.$parent) : rawId;
 
 546   // Register the component.
 
 547   self.destroy = $mdComponentRegistry.register(self, componentId);
 
 549   // Watch and update the component, if the id has changed.
 
 550   if (hasDataBinding) {
 
 551     $attrs.$observe('mdComponentId', function(id) {
 
 552       if (id && id !== self.$$mdHandle) {
 
 553         self.destroy(); // `destroy` only deregisters the old component id so we can add the new one.
 
 554         self.destroy = $mdComponentRegistry.register(self, id);
 
 560 })(window, window.angular);