sidenav.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v0.11.4
  6. */
  7. (function( window, angular, undefined ){
  8. "use strict";
  9. /**
  10. * @ngdoc module
  11. * @name material.components.sidenav
  12. *
  13. * @description
  14. * A Sidenav QP component.
  15. */
  16. angular
  17. .module('material.components.sidenav', [
  18. 'material.core',
  19. 'material.components.backdrop'
  20. ])
  21. .factory('$mdSidenav', SidenavService )
  22. .directive('mdSidenav', SidenavDirective)
  23. .directive('mdSidenavFocus', SidenavFocusDirective)
  24. .controller('$mdSidenavController', SidenavController);
  25. /**
  26. * @ngdoc service
  27. * @name $mdSidenav
  28. * @module material.components.sidenav
  29. *
  30. * @description
  31. * `$mdSidenav` makes it easy to interact with multiple sidenavs
  32. * in an app.
  33. *
  34. * @usage
  35. * <hljs lang="js">
  36. * // Async lookup for sidenav instance; will resolve when the instance is available
  37. * $mdSidenav(componentId).then(function(instance) {
  38. * $log.debug( componentId + "is now ready" );
  39. * });
  40. * // Async toggle the given sidenav;
  41. * // when instance is known ready and lazy lookup is not needed.
  42. * $mdSidenav(componentId)
  43. * .toggle()
  44. * .then(function(){
  45. * $log.debug('toggled');
  46. * });
  47. * // Async open the given sidenav
  48. * $mdSidenav(componentId)
  49. * .open()
  50. * .then(function(){
  51. * $log.debug('opened');
  52. * });
  53. * // Async close the given sidenav
  54. * $mdSidenav(componentId)
  55. * .close()
  56. * .then(function(){
  57. * $log.debug('closed');
  58. * });
  59. * // Sync check to see if the specified sidenav is set to be open
  60. * $mdSidenav(componentId).isOpen();
  61. * // Sync check to whether given sidenav is locked open
  62. * // If this is true, the sidenav will be open regardless of close()
  63. * $mdSidenav(componentId).isLockedOpen();
  64. * </hljs>
  65. */
  66. function SidenavService($mdComponentRegistry, $q) {
  67. return function(handle) {
  68. // Lookup the controller instance for the specified sidNav instance
  69. var self;
  70. var errorMsg = "SideNav '" + handle + "' is not available!";
  71. var instance = $mdComponentRegistry.get(handle);
  72. if(!instance) {
  73. $mdComponentRegistry.notFoundError(handle);
  74. }
  75. return self = {
  76. // -----------------
  77. // Sync methods
  78. // -----------------
  79. isOpen: function() {
  80. return instance && instance.isOpen();
  81. },
  82. isLockedOpen: function() {
  83. return instance && instance.isLockedOpen();
  84. },
  85. // -----------------
  86. // Async methods
  87. // -----------------
  88. toggle: function() {
  89. return instance ? instance.toggle() : $q.reject(errorMsg);
  90. },
  91. open: function() {
  92. return instance ? instance.open() : $q.reject(errorMsg);
  93. },
  94. close: function() {
  95. return instance ? instance.close() : $q.reject(errorMsg);
  96. },
  97. then : function( callbackFn ) {
  98. var promise = instance ? $q.when(instance) : waitForInstance();
  99. return promise.then( callbackFn || angular.noop );
  100. }
  101. };
  102. /**
  103. * Deferred lookup of component instance using $component registry
  104. */
  105. function waitForInstance() {
  106. return $mdComponentRegistry
  107. .when(handle)
  108. .then(function( it ){
  109. instance = it;
  110. return it;
  111. });
  112. }
  113. };
  114. }
  115. SidenavService.$inject = ["$mdComponentRegistry", "$q"];
  116. /**
  117. * @ngdoc directive
  118. * @name mdSidenavFocus
  119. * @module material.components.sidenav
  120. *
  121. * @restrict A
  122. *
  123. * @description
  124. * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens.
  125. * This is completely optional, as the sidenav itself is focused by default.
  126. *
  127. * @usage
  128. * <hljs lang="html">
  129. * <md-sidenav>
  130. * <form>
  131. * <md-input-container>
  132. * <label for="testInput">Label</label>
  133. * <input id="testInput" type="text" md-sidenav-focus>
  134. * </md-input-container>
  135. * </form>
  136. * </md-sidenav>
  137. * </hljs>
  138. **/
  139. function SidenavFocusDirective() {
  140. return {
  141. restrict: 'A',
  142. require: '^mdSidenav',
  143. link: function(scope, element, attr, sidenavCtrl) {
  144. // @see $mdUtil.findFocusTarget(...)
  145. }
  146. };
  147. }
  148. /**
  149. * @ngdoc directive
  150. * @name mdSidenav
  151. * @module material.components.sidenav
  152. * @restrict E
  153. *
  154. * @description
  155. *
  156. * A Sidenav component that can be opened and closed programatically.
  157. *
  158. * By default, upon opening it will slide out on top of the main content area.
  159. *
  160. * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
  161. * It can be overridden with the `md-autofocus` directive on the child element you want focused.
  162. *
  163. * @usage
  164. * <hljs lang="html">
  165. * <div layout="row" ng-controller="MyController">
  166. * <md-sidenav md-component-id="left" class="md-sidenav-left">
  167. * Left Nav!
  168. * </md-sidenav>
  169. *
  170. * <md-content>
  171. * Center Content
  172. * <md-button ng-click="openLeftMenu()">
  173. * Open Left Menu
  174. * </md-button>
  175. * </md-content>
  176. *
  177. * <md-sidenav md-component-id="right"
  178. * md-is-locked-open="$mdMedia('min-width: 333px')"
  179. * class="md-sidenav-right">
  180. * <form>
  181. * <md-input-container>
  182. * <label for="testInput">Test input</label>
  183. * <input id="testInput" type="text"
  184. * ng-model="data" md-autofocus>
  185. * </md-input-container>
  186. * </form>
  187. * </md-sidenav>
  188. * </div>
  189. * </hljs>
  190. *
  191. * <hljs lang="js">
  192. * var app = angular.module('myApp', ['ngMaterial']);
  193. * app.controller('MyController', function($scope, $mdSidenav) {
  194. * $scope.openLeftMenu = function() {
  195. * $mdSidenav('left').toggle();
  196. * };
  197. * });
  198. * </hljs>
  199. *
  200. * @param {expression=} md-is-open A model bound to whether the sidenav is opened.
  201. * @param {string=} md-component-id componentId to use with $mdSidenav service.
  202. * @param {expression=} md-is-locked-open When this expression evalutes to true,
  203. * the sidenav 'locks open': it falls into the content's flow instead
  204. * of appearing over it. This overrides the `is-open` attribute.
  205. *
  206. * The $mdMedia() service is exposed to the is-locked-open attribute, which
  207. * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets.
  208. * Examples:
  209. *
  210. * - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
  211. * - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
  212. * - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
  213. */
  214. function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
  215. return {
  216. restrict: 'E',
  217. scope: {
  218. isOpen: '=?mdIsOpen'
  219. },
  220. controller: '$mdSidenavController',
  221. compile: function(element) {
  222. element.addClass('md-closed');
  223. element.attr('tabIndex', '-1');
  224. return postLink;
  225. }
  226. };
  227. /**
  228. * Directive Post Link function...
  229. */
  230. function postLink(scope, element, attr, sidenavCtrl) {
  231. var lastParentOverFlow;
  232. var triggeringElement = null;
  233. var promise = $q.when(true);
  234. var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
  235. var isLocked = function() {
  236. return isLockedOpenParsed(scope.$parent, {
  237. $media: function(arg) {
  238. $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead.");
  239. return $mdMedia(arg);
  240. },
  241. $mdMedia: $mdMedia
  242. });
  243. };
  244. var backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
  245. $mdTheming.inherit(backdrop, element);
  246. element.on('$destroy', function() {
  247. backdrop.remove();
  248. sidenavCtrl.destroy();
  249. });
  250. scope.$on('$destroy', function(){
  251. backdrop.remove()
  252. });
  253. scope.$watch(isLocked, updateIsLocked);
  254. scope.$watch('isOpen', updateIsOpen);
  255. // Publish special accessor for the Controller instance
  256. sidenavCtrl.$toggleOpen = toggleOpen;
  257. /**
  258. * Toggle the DOM classes to indicate `locked`
  259. * @param isLocked
  260. */
  261. function updateIsLocked(isLocked, oldValue) {
  262. scope.isLockedOpen = isLocked;
  263. if (isLocked === oldValue) {
  264. element.toggleClass('md-locked-open', !!isLocked);
  265. } else {
  266. $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
  267. }
  268. backdrop.toggleClass('md-locked-open', !!isLocked);
  269. }
  270. /**
  271. * Toggle the SideNav view and attach/detach listeners
  272. * @param isOpen
  273. */
  274. function updateIsOpen(isOpen) {
  275. // Support deprecated md-sidenav-focus attribute as fallback
  276. var focusEl = $mdUtil.findFocusTarget(element) || $mdUtil.findFocusTarget(element,'[md-sidenav-focus]') || element;
  277. var parent = element.parent();
  278. parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
  279. backdrop[isOpen ? 'on' : 'off']('click', close);
  280. if ( isOpen ) {
  281. // Capture upon opening..
  282. triggeringElement = $document[0].activeElement;
  283. }
  284. disableParentScroll(isOpen);
  285. return promise = $q.all([
  286. isOpen ? $animate.enter(backdrop, parent) : $animate.leave(backdrop),
  287. $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
  288. ])
  289. .then(function() {
  290. // Perform focus when animations are ALL done...
  291. if (scope.isOpen) {
  292. focusEl && focusEl.focus();
  293. }
  294. });
  295. }
  296. /**
  297. * Prevent parent scrolling (when the SideNav is open)
  298. */
  299. function disableParentScroll(disabled) {
  300. var parent = element.parent();
  301. if ( disabled && !lastParentOverFlow ) {
  302. lastParentOverFlow = parent.css('overflow');
  303. parent.css('overflow', 'hidden');
  304. } else if (angular.isDefined(lastParentOverFlow)) {
  305. parent.css('overflow', lastParentOverFlow);
  306. lastParentOverFlow = undefined;
  307. }
  308. }
  309. /**
  310. * Toggle the sideNav view and publish a promise to be resolved when
  311. * the view animation finishes.
  312. *
  313. * @param isOpen
  314. * @returns {*}
  315. */
  316. function toggleOpen( isOpen ) {
  317. if (scope.isOpen == isOpen ) {
  318. return $q.when(true);
  319. } else {
  320. return $q(function(resolve){
  321. // Toggle value to force an async `updateIsOpen()` to run
  322. scope.isOpen = isOpen;
  323. $mdUtil.nextTick(function() {
  324. // When the current `updateIsOpen()` animation finishes
  325. promise.then(function(result) {
  326. if ( !scope.isOpen ) {
  327. // reset focus to originating element (if available) upon close
  328. triggeringElement && triggeringElement.focus();
  329. triggeringElement = null;
  330. }
  331. resolve(result);
  332. });
  333. });
  334. });
  335. }
  336. }
  337. /**
  338. * Auto-close sideNav when the `escape` key is pressed.
  339. * @param evt
  340. */
  341. function onKeyDown(ev) {
  342. var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
  343. return isEscape ? close(ev) : $q.when(true);
  344. }
  345. /**
  346. * With backdrop `clicks` or `escape` key-press, immediately
  347. * apply the CSS close transition... Then notify the controller
  348. * to close() and perform its own actions.
  349. */
  350. function close(ev) {
  351. ev.preventDefault();
  352. ev.stopPropagation();
  353. return sidenavCtrl.close();
  354. }
  355. }
  356. }
  357. SidenavDirective.$inject = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$animate", "$compile", "$parse", "$log", "$q", "$document"];
  358. /*
  359. * @private
  360. * @ngdoc controller
  361. * @name SidenavController
  362. * @module material.components.sidenav
  363. *
  364. */
  365. function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) {
  366. var self = this;
  367. // Use Default internal method until overridden by directive postLink
  368. // Synchronous getters
  369. self.isOpen = function() { return !!$scope.isOpen; };
  370. self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
  371. // Async actions
  372. self.open = function() { return self.$toggleOpen( true ); };
  373. self.close = function() { return self.$toggleOpen( false ); };
  374. self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); };
  375. self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); };
  376. self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId);
  377. }
  378. SidenavController.$inject = ["$scope", "$element", "$attrs", "$mdComponentRegistry", "$q"];
  379. })(window, window.angular);