f82c9fc099b7873803350297daed718fbb025240
[vnfsdk/refrepo.git] /
1 /*!
2  * Angular Material Design
3  * https://github.com/angular/material
4  * @license MIT
5  * v1.1.3
6  */
7 goog.provide('ngmaterial.components.sidenav');
8 goog.require('ngmaterial.components.backdrop');
9 goog.require('ngmaterial.core');
10 /**
11  * @ngdoc module
12  * @name material.components.sidenav
13  *
14  * @description
15  * A Sidenav QP component.
16  */
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"];
20 angular
21   .module('material.components.sidenav', [
22     'material.core',
23     'material.components.backdrop'
24   ])
25   .factory('$mdSidenav', SidenavService )
26   .directive('mdSidenav', SidenavDirective)
27   .directive('mdSidenavFocus', SidenavFocusDirective)
28   .controller('$mdSidenavController', SidenavController);
29
30
31 /**
32  * @ngdoc service
33  * @name $mdSidenav
34  * @module material.components.sidenav
35  *
36  * @description
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`.
41  *
42  * @usage
43  * <hljs lang="js">
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" );
47  * });
48  * // Sync lookup for sidenav instance; this will resolve immediately.
49  * $mdSidenav(componentId).then(function(instance) {
50  *   $log.debug( componentId + "is now ready" );
51  * });
52  * // Async toggle the given sidenav;
53  * // when instance is known ready and lazy lookup is not needed.
54  * $mdSidenav(componentId)
55  *    .toggle()
56  *    .then(function(){
57  *      $log.debug('toggled');
58  *    });
59  * // Async open the given sidenav
60  * $mdSidenav(componentId)
61  *    .open()
62  *    .then(function(){
63  *      $log.debug('opened');
64  *    });
65  * // Async close the given sidenav
66  * $mdSidenav(componentId)
67  *    .close()
68  *    .then(function(){
69  *      $log.debug('closed');
70  *    });
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');
80  * });
81  * </hljs>
82  */
83 function SidenavService($mdComponentRegistry, $mdUtil, $q, $log) {
84   var errorMsg = "SideNav '{0}' is not available! Did you use md-component-id='{0}'?";
85   var service = {
86         find    : findInstance,     //  sync  - returns proxy API
87         waitFor : waitForInstance   //  async - returns promise
88       };
89
90   /**
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
95    *    left.toggle();
96    *   });
97    */
98   return function(handle, enableWait) {
99     if ( angular.isUndefined(handle) ) return service;
100
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;
105   };
106
107   /**
108    * For failed instance/handle lookups, older-clients expect an response object with noops
109    * that include `rejected promise APIs`
110    */
111   function addLegacyAPI(service, handle) {
112       var falseFn  = function() { return false; };
113       var rejectFn = function() {
114             return $q.when($mdUtil.supplant(errorMsg, [handle || ""]));
115           };
116
117       return angular.extend({
118         isLockedOpen : falseFn,
119         isOpen       : falseFn,
120         toggle       : rejectFn,
121         open         : rejectFn,
122         close        : rejectFn,
123         onClose      : angular.noop,
124         then : function(callback) {
125           return waitForInstance(handle)
126             .then(callback || angular.noop);
127         }
128        }, service);
129     }
130     /**
131      * Synchronously lookup the controller instance for the specified sidNav instance which has been
132      * registered with the markup `md-component-id`
133      */
134     function findInstance(handle, shouldWait) {
135       var instance = $mdComponentRegistry.get(handle);
136
137       if (!instance && !shouldWait) {
138
139         // Report missing instance
140         $log.error( $mdUtil.supplant(errorMsg, [handle || ""]) );
141
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
144         return undefined;
145       }
146       return instance;
147     }
148
149     /**
150      * Asynchronously wait for the component instantiation,
151      * Deferred lookup of component instance using $component registry
152      */
153     function waitForInstance(handle) {
154       return $mdComponentRegistry.when(handle).catch($log.error);
155     }
156 }
157 /**
158  * @ngdoc directive
159  * @name mdSidenavFocus
160  * @module material.components.sidenav
161  *
162  * @restrict A
163  *
164  * @description
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.
167  *
168  * @usage
169  * <hljs lang="html">
170  * <md-sidenav>
171  *   <form>
172  *     <md-input-container>
173  *       <label for="testInput">Label</label>
174  *       <input id="testInput" type="text" md-sidenav-focus>
175  *     </md-input-container>
176  *   </form>
177  * </md-sidenav>
178  * </hljs>
179  **/
180 function SidenavFocusDirective() {
181   return {
182     restrict: 'A',
183     require: '^mdSidenav',
184     link: function(scope, element, attr, sidenavCtrl) {
185       // @see $mdUtil.findFocusTarget(...)
186     }
187   };
188 }
189 /**
190  * @ngdoc directive
191  * @name mdSidenav
192  * @module material.components.sidenav
193  * @restrict E
194  *
195  * @description
196  *
197  * A Sidenav component that can be opened and closed programatically.
198  *
199  * By default, upon opening it will slide out on top of the main content area.
200  *
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.
203  *
204  * @usage
205  * <hljs lang="html">
206  * <div layout="row" ng-controller="MyController">
207  *   <md-sidenav md-component-id="left" class="md-sidenav-left">
208  *     Left Nav!
209  *   </md-sidenav>
210  *
211  *   <md-content>
212  *     Center Content
213  *     <md-button ng-click="openLeftMenu()">
214  *       Open Left Menu
215  *     </md-button>
216  *   </md-content>
217  *
218  *   <md-sidenav md-component-id="right"
219  *     md-is-locked-open="$mdMedia('min-width: 333px')"
220  *     class="md-sidenav-right">
221  *     <form>
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>
227  *     </form>
228  *   </md-sidenav>
229  * </div>
230  * </hljs>
231  *
232  * <hljs lang="js">
233  * var app = angular.module('myApp', ['ngMaterial']);
234  * app.controller('MyController', function($scope, $mdSidenav) {
235  *   $scope.openLeftMenu = function() {
236  *     $mdSidenav('left').toggle();
237  *   };
238  * });
239  * </hljs>
240  *
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.
249  *
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.
252  * Examples:
253  *
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)
257  */
258 function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate,
259                           $compile, $parse, $log, $q, $document, $window, $$rAF) {
260   return {
261     restrict: 'E',
262     scope: {
263       isOpen: '=?mdIsOpen'
264     },
265     controller: '$mdSidenavController',
266     compile: function(element) {
267       element.addClass('md-closed').attr('tabIndex', '-1');
268       return postLink;
269     }
270   };
271
272   /**
273    * Directive Post Link function...
274    */
275   function postLink(scope, element, attr, sidenavCtrl) {
276     var lastParentOverFlow;
277     var backdrop;
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);
290         },
291         $mdMedia: $mdMedia
292       });
293     };
294
295     if (attr.mdDisableScrollTarget) {
296       disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget);
297
298       if (disableScrollTarget) {
299         disableScrollTarget = angular.element(disableScrollTarget);
300       } else {
301         $log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' +
302           'selector "{selector}". Falling back to parent.', { selector: attr.mdDisableScrollTarget }));
303       }
304     }
305
306     if (!disableScrollTarget) {
307       disableScrollTarget = element.parent();
308     }
309
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");
313     }
314
315     element.addClass('_md');     // private md component indicator for styling
316     $mdTheming(element);
317
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);
321
322     element.on('$destroy', function() {
323       backdrop && backdrop.remove();
324       sidenavCtrl.destroy();
325     });
326
327     scope.$on('$destroy', function(){
328       backdrop && backdrop.remove();
329     });
330
331     scope.$watch(isLocked, updateIsLocked);
332     scope.$watch('isOpen', updateIsOpen);
333
334
335     // Publish special accessor for the Controller instance
336     sidenavCtrl.$toggleOpen = toggleOpen;
337
338     /**
339      * Toggle the DOM classes to indicate `locked`
340      * @param isLocked
341      */
342     function updateIsLocked(isLocked, oldValue) {
343       scope.isLockedOpen = isLocked;
344       if (isLocked === oldValue) {
345         element.toggleClass('md-locked-open', !!isLocked);
346       } else {
347         $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
348       }
349       if (backdrop) {
350         backdrop.toggleClass('md-locked-open', !!isLocked);
351       }
352     }
353
354     /**
355      * Toggle the SideNav view and attach/detach listeners
356      * @param isOpen
357      */
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();
362
363       parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
364       if (backdrop) backdrop[isOpen ? 'on' : 'off']('click', close);
365
366       var restorePositioning = updateContainerPositions(parent, isOpen);
367
368       if ( isOpen ) {
369         // Capture upon opening..
370         triggeringElement = $document[0].activeElement;
371         triggeringInteractionType = $mdInteraction.getLastInteractionType();
372       }
373
374       disableParentScroll(isOpen);
375
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')
380       ]).then(function() {
381         // Perform focus when animations are ALL done...
382         if (scope.isOpen) {
383           $$rAF(function() {
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');
387           });
388
389           focusEl && focusEl.focus();
390         }
391
392         // Restores the positioning on the sidenav and backdrop.
393         restorePositioning && restorePositioning();
394       });
395     }
396
397     function updateContainerPositions(parent, willOpen) {
398       var drawerEl = element[0];
399       var scrollTop = parent[0].scrollTop;
400
401       if (willOpen && scrollTop) {
402         previousContainerStyles = {
403           top: drawerEl.style.top,
404           bottom: drawerEl.style.bottom,
405           height: drawerEl.style.height
406         };
407
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',
413           bottom: 'auto',
414           height: parent[0].clientHeight + 'px'
415         };
416
417         // Apply the new position styles to the sidenav and backdrop.
418         element.css(positionStyle);
419         backdrop.css(positionStyle);
420       }
421
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) {
425         return function() {
426           drawerEl.style.top = previousContainerStyles.top;
427           drawerEl.style.bottom = previousContainerStyles.bottom;
428           drawerEl.style.height = previousContainerStyles.height;
429
430           backdrop[0].style.top = null;
431           backdrop[0].style.bottom = null;
432           backdrop[0].style.height = null;
433
434           previousContainerStyles = null;
435         };
436       }
437     }
438
439     /**
440      * Prevent parent scrolling (when the SideNav is open)
441      */
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;
449       }
450     }
451
452     /**
453      * Toggle the sideNav view and publish a promise to be resolved when
454      * the view animation finishes.
455      *
456      * @param isOpen
457      * @returns {*}
458      */
459     function toggleOpen( isOpen ) {
460       if (scope.isOpen == isOpen ) {
461
462         return $q.when(true);
463
464       } else {
465         if (scope.isOpen && sidenavCtrl.onCloseCb) sidenavCtrl.onCloseCb();
466
467         return $q(function(resolve){
468           // Toggle value to force an async `updateIsOpen()` to run
469           scope.isOpen = isOpen;
470
471           $mdUtil.nextTick(function() {
472             // When the current `updateIsOpen()` animation finishes
473             promise.then(function(result) {
474
475               if ( !scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') {
476                 // reset focus to originating element (if available) upon close
477                 triggeringElement.focus();
478                 triggeringElement = null;
479               }
480
481               resolve(result);
482             });
483           });
484
485         });
486
487       }
488     }
489
490     /**
491      * Auto-close sideNav when the `escape` key is pressed.
492      * @param evt
493      */
494     function onKeyDown(ev) {
495       var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
496       return isEscape ? close(ev) : $q.when(true);
497     }
498
499     /**
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.
503      */
504     function close(ev) {
505       ev.preventDefault();
506
507       return sidenavCtrl.close();
508     }
509
510   }
511 }
512
513 /*
514  * @private
515  * @ngdoc controller
516  * @name SidenavController
517  * @module material.components.sidenav
518  */
519 function SidenavController($scope, $attrs, $mdComponentRegistry, $q, $interpolate) {
520
521   var self = this;
522
523   // Use Default internal method until overridden by directive postLink
524
525   // Synchronous getters
526   self.isOpen = function() { return !!$scope.isOpen; };
527   self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
528
529   // Synchronous setters
530   self.onClose = function (callback) {
531     self.onCloseCb = callback;
532     return self;
533   };
534
535   // Async actions
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); };
540
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;
545
546   // Register the component.
547   self.destroy = $mdComponentRegistry.register(self, componentId);
548
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);
555       }
556     });
557   }
558 }
559
560 ngmaterial.components.sidenav = angular.module("material.components.sidenav");