2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ng.material.components.sidenav');
8 goog.require('ng.material.components.backdrop');
9 goog.require('ng.material.core');
12 * @name material.components.sidenav
15 * A Sidenav QP component.
17 angular.module('material.components.sidenav', [
19 'material.components.backdrop'
21 .factory('$mdSidenav', SidenavService )
22 .directive('mdSidenav', SidenavDirective)
23 .directive('mdSidenavFocus', SidenavFocusDirective)
24 .controller('$mdSidenavController', SidenavController);
31 * @module material.components.sidenav
34 * `$mdSidenav` makes it easy to interact with multiple sidenavs
39 * // Async lookup for sidenav instance; will resolve when the instance is available
40 * $mdSidenav(componentId).then(function(instance) {
41 * $log.debug( componentId + "is now ready" );
43 * // Async toggle the given sidenav;
44 * // when instance is known ready and lazy lookup is not needed.
45 * $mdSidenav(componentId)
48 * $log.debug('toggled');
50 * // Async open the given sidenav
51 * $mdSidenav(componentId)
54 * $log.debug('opened');
56 * // Async close the given sidenav
57 * $mdSidenav(componentId)
60 * $log.debug('closed');
62 * // Sync check to see if the specified sidenav is set to be open
63 * $mdSidenav(componentId).isOpen();
64 * // Sync check to whether given sidenav is locked open
65 * // If this is true, the sidenav will be open regardless of close()
66 * $mdSidenav(componentId).isLockedOpen();
69 function SidenavService($mdComponentRegistry, $q) {
70 return function(handle) {
72 // Lookup the controller instance for the specified sidNav instance
74 var errorMsg = "SideNav '" + handle + "' is not available!";
75 var instance = $mdComponentRegistry.get(handle);
78 $mdComponentRegistry.notFoundError(handle);
86 return instance && instance.isOpen();
88 isLockedOpen: function() {
89 return instance && instance.isLockedOpen();
95 return instance ? instance.toggle() : $q.reject(errorMsg);
98 return instance ? instance.open() : $q.reject(errorMsg);
101 return instance ? instance.close() : $q.reject(errorMsg);
103 then : function( callbackFn ) {
104 var promise = instance ? $q.when(instance) : waitForInstance();
105 return promise.then( callbackFn || angular.noop );
110 * Deferred lookup of component instance using $component registry
112 function waitForInstance() {
113 return $mdComponentRegistry
115 .then(function( it ){
122 SidenavService.$inject = ["$mdComponentRegistry", "$q"];
125 * @name mdSidenavFocus
126 * @module material.components.sidenav
131 * `$mdSidenavFocus` provides a way to specify the focused element when a sidenav opens.
132 * This is completely optional, as the sidenav itself is focused by default.
138 * <md-input-container>
139 * <label for="testInput">Label</label>
140 * <input id="testInput" type="text" md-sidenav-focus>
141 * </md-input-container>
146 function SidenavFocusDirective() {
149 require: '^mdSidenav',
150 link: function(scope, element, attr, sidenavCtrl) {
151 sidenavCtrl.focusElement(element);
158 * @module material.components.sidenav
163 * A Sidenav component that can be opened and closed programatically.
165 * By default, upon opening it will slide out on top of the main content area.
167 * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
168 * It can be overridden with the `md-sidenav-focus` directive on the child element you want focused.
172 * <div layout="row" ng-controller="MyController">
173 * <md-sidenav md-component-id="left" class="md-sidenav-left">
179 * <md-button ng-click="openLeftMenu()">
184 * <md-sidenav md-component-id="right"
185 * md-is-locked-open="$mdMedia('min-width: 333px')"
186 * class="md-sidenav-right">
188 * <md-input-container>
189 * <label for="testInput">Test input</label>
190 * <input id="testInput" type="text"
191 * ng-model="data" md-sidenav-focus>
192 * </md-input-container>
199 * var app = angular.module('myApp', ['ngMaterial']);
200 * app.controller('MyController', function($scope, $mdSidenav) {
201 * $scope.openLeftMenu = function() {
202 * $mdSidenav('left').toggle();
207 * @param {expression=} md-is-open A model bound to whether the sidenav is opened.
208 * @param {string=} md-component-id componentId to use with $mdSidenav service.
209 * @param {expression=} md-is-locked-open When this expression evalutes to true,
210 * the sidenav 'locks open': it falls into the content's flow instead
211 * of appearing over it. This overrides the `is-open` attribute.
213 * The $mdMedia() service is exposed to the is-locked-open attribute, which
214 * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets.
217 * - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
218 * - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
219 * - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
221 function SidenavDirective($timeout, $animate, $parse, $log, $mdMedia, $mdConstant, $compile, $mdTheming, $q, $document) {
227 controller: '$mdSidenavController',
228 compile: function(element) {
229 element.addClass('md-closed');
230 element.attr('tabIndex', '-1');
236 * Directive Post Link function...
238 function postLink(scope, element, attr, sidenavCtrl) {
239 var lastParentOverFlow;
240 var triggeringElement = null;
241 var promise = $q.when(true);
243 var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
244 var isLocked = function() {
245 return isLockedOpenParsed(scope.$parent, {
246 $media: function(arg) {
247 $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead.");
248 return $mdMedia(arg);
253 var backdrop = $compile(
254 '<md-backdrop class="md-sidenav-backdrop md-opaque ng-enter">'
257 element.on('$destroy', sidenavCtrl.destroy);
258 $mdTheming.inherit(backdrop, element);
260 scope.$watch(isLocked, updateIsLocked);
261 scope.$watch('isOpen', updateIsOpen);
264 // Publish special accessor for the Controller instance
265 sidenavCtrl.$toggleOpen = toggleOpen;
266 sidenavCtrl.focusElement( sidenavCtrl.focusElement() || element );
269 * Toggle the DOM classes to indicate `locked`
272 function updateIsLocked(isLocked, oldValue) {
273 scope.isLockedOpen = isLocked;
274 if (isLocked === oldValue) {
275 element.toggleClass('md-locked-open', !!isLocked);
277 $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
279 backdrop.toggleClass('md-locked-open', !!isLocked);
283 * Toggle the SideNav view and attach/detach listeners
286 function updateIsOpen(isOpen) {
287 var parent = element.parent();
289 parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
290 backdrop[isOpen ? 'on' : 'off']('click', close);
293 // Capture upon opening..
294 triggeringElement = $document[0].activeElement;
296 var focusEl = sidenavCtrl.focusElement();
298 disableParentScroll(isOpen);
300 return promise = $q.all([
301 isOpen ? $animate.enter(backdrop, parent) : $animate.leave(backdrop),
302 $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
305 // Perform focus when animations are ALL done...
307 focusEl && focusEl.focus();
313 * Prevent parent scrolling (when the SideNav is open)
315 function disableParentScroll(disabled) {
316 var parent = element.parent();
318 lastParentOverFlow = parent.css('overflow');
319 parent.css('overflow', 'hidden');
320 } else if (angular.isDefined(lastParentOverFlow)) {
321 parent.css('overflow', lastParentOverFlow);
322 lastParentOverFlow = undefined;
327 * Toggle the sideNav view and publish a promise to be resolved when
328 * the view animation finishes.
333 function toggleOpen( isOpen ) {
334 if (scope.isOpen == isOpen ) {
336 return $q.when(true);
339 var deferred = $q.defer();
341 // Toggle value to force an async `updateIsOpen()` to run
342 scope.isOpen = isOpen;
344 $timeout(function() {
346 // When the current `updateIsOpen()` animation finishes
347 promise.then(function(result) {
349 if ( !scope.isOpen ) {
350 // reset focus to originating element (if available) upon close
351 triggeringElement && triggeringElement.focus();
352 triggeringElement = null;
355 deferred.resolve(result);
360 return deferred.promise;
365 * Auto-close sideNav when the `escape` key is pressed.
368 function onKeyDown(ev) {
369 var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
370 return isEscape ? close(ev) : $q.when(true);
374 * With backdrop `clicks` or `escape` key-press, immediately
375 * apply the CSS close transition... Then notify the controller
376 * to close() and perform its own actions.
380 ev.stopPropagation();
382 return sidenavCtrl.close();
387 SidenavDirective.$inject = ["$timeout", "$animate", "$parse", "$log", "$mdMedia", "$mdConstant", "$compile", "$mdTheming", "$q", "$document"];
392 * @name SidenavController
393 * @module material.components.sidenav
396 function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) {
401 // Use Default internal method until overridden by directive postLink
403 // Synchronous getters
404 self.isOpen = function() { return !!$scope.isOpen; };
405 self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
408 self.open = function() { return self.$toggleOpen( true ); };
409 self.close = function() { return self.$toggleOpen( false ); };
410 self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); };
411 self.focusElement = function(el) {
412 if ( angular.isDefined(el) ) {
418 self.$toggleOpen = function() { return $q.when($scope.isOpen); };
420 self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId);
422 SidenavController.$inject = ["$scope", "$element", "$attrs", "$mdComponentRegistry", "$q"];
424 ng.material.components.sidenav = angular.module("material.components.sidenav");