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