You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

535 lines
17 KiB

  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v1.1.1
  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", "$animate", "$compile", "$parse", "$log", "$q", "$document"];
  19. SidenavController.$inject = ["$scope", "$element", "$attrs", "$mdComponentRegistry", "$q"];
  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. * @ngdoc service
  31. * @name $mdSidenav
  32. * @module material.components.sidenav
  33. *
  34. * @description
  35. * `$mdSidenav` makes it easy to interact with multiple sidenavs
  36. * in an app. When looking up a sidenav instance, you can either look
  37. * it up synchronously or wait for it to be initializied asynchronously.
  38. * This is done by passing the second argument to `$mdSidenav`.
  39. *
  40. * @usage
  41. * <hljs lang="js">
  42. * // Async lookup for sidenav instance; will resolve when the instance is available
  43. * $mdSidenav(componentId, true).then(function(instance) {
  44. * $log.debug( componentId + "is now ready" );
  45. * });
  46. * // Sync lookup for sidenav instance; this will resolve immediately.
  47. * $mdSidenav(componentId).then(function(instance) {
  48. * $log.debug( componentId + "is now ready" );
  49. * });
  50. * // Async toggle the given sidenav;
  51. * // when instance is known ready and lazy lookup is not needed.
  52. * $mdSidenav(componentId)
  53. * .toggle()
  54. * .then(function(){
  55. * $log.debug('toggled');
  56. * });
  57. * // Async open the given sidenav
  58. * $mdSidenav(componentId)
  59. * .open()
  60. * .then(function(){
  61. * $log.debug('opened');
  62. * });
  63. * // Async close the given sidenav
  64. * $mdSidenav(componentId)
  65. * .close()
  66. * .then(function(){
  67. * $log.debug('closed');
  68. * });
  69. * // Sync check to see if the specified sidenav is set to be open
  70. * $mdSidenav(componentId).isOpen();
  71. * // Sync check to whether given sidenav is locked open
  72. * // If this is true, the sidenav will be open regardless of close()
  73. * $mdSidenav(componentId).isLockedOpen();
  74. * // On close callback to handle close, backdrop click or escape key pressed
  75. * // Callback happens BEFORE the close action occurs.
  76. * $mdSidenav(componentId).onClose(function () {
  77. * $log.debug('closing');
  78. * });
  79. * </hljs>
  80. */
  81. function SidenavService($mdComponentRegistry, $mdUtil, $q, $log) {
  82. var errorMsg = "SideNav '{0}' is not available! Did you use md-component-id='{0}'?";
  83. var service = {
  84. find : findInstance, // sync - returns proxy API
  85. waitFor : waitForInstance // async - returns promise
  86. };
  87. /**
  88. * Service API that supports three (3) usages:
  89. * $mdSidenav().find("left") // sync (must already exist) or returns undefined
  90. * $mdSidenav("left").toggle(); // sync (must already exist) or returns reject promise;
  91. * $mdSidenav("left",true).then( function(left){ // async returns instance when available
  92. * left.toggle();
  93. * });
  94. */
  95. return function(handle, enableWait) {
  96. if ( angular.isUndefined(handle) ) return service;
  97. var shouldWait = enableWait === true;
  98. var instance = service.find(handle, shouldWait);
  99. return !instance && shouldWait ? service.waitFor(handle) :
  100. !instance && angular.isUndefined(enableWait) ? addLegacyAPI(service, handle) : instance;
  101. };
  102. /**
  103. * For failed instance/handle lookups, older-clients expect an response object with noops
  104. * that include `rejected promise APIs`
  105. */
  106. function addLegacyAPI(service, handle) {
  107. var falseFn = function() { return false; };
  108. var rejectFn = function() {
  109. return $q.when($mdUtil.supplant(errorMsg, [handle || ""]));
  110. };
  111. return angular.extend({
  112. isLockedOpen : falseFn,
  113. isOpen : falseFn,
  114. toggle : rejectFn,
  115. open : rejectFn,
  116. close : rejectFn,
  117. onClose : angular.noop,
  118. then : function(callback) {
  119. return waitForInstance(handle)
  120. .then(callback || angular.noop);
  121. }
  122. }, service);
  123. }
  124. /**
  125. * Synchronously lookup the controller instance for the specified sidNav instance which has been
  126. * registered with the markup `md-component-id`
  127. */
  128. function findInstance(handle, shouldWait) {
  129. var instance = $mdComponentRegistry.get(handle);
  130. if (!instance && !shouldWait) {
  131. // Report missing instance
  132. $log.error( $mdUtil.supplant(errorMsg, [handle || ""]) );
  133. // The component has not registered itself... most like NOT yet created
  134. // return null to indicate that the Sidenav is not in the DOM
  135. return undefined;
  136. }
  137. return instance;
  138. }
  139. /**
  140. * Asynchronously wait for the component instantiation,
  141. * Deferred lookup of component instance using $component registry
  142. */
  143. function waitForInstance(handle) {
  144. return $mdComponentRegistry.when(handle).catch($log.error);
  145. }
  146. }
  147. /**
  148. * @ngdoc directive
  149. * @name mdSidenavFocus
  150. * @module material.components.sidenav
  151. *
  152. * @restrict A
  153. *
  154. * @description
  155. * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens.
  156. * This is completely optional, as the sidenav itself is focused by default.
  157. *
  158. * @usage
  159. * <hljs lang="html">
  160. * <md-sidenav>
  161. * <form>
  162. * <md-input-container>
  163. * <label for="testInput">Label</label>
  164. * <input id="testInput" type="text" md-sidenav-focus>
  165. * </md-input-container>
  166. * </form>
  167. * </md-sidenav>
  168. * </hljs>
  169. **/
  170. function SidenavFocusDirective() {
  171. return {
  172. restrict: 'A',
  173. require: '^mdSidenav',
  174. link: function(scope, element, attr, sidenavCtrl) {
  175. // @see $mdUtil.findFocusTarget(...)
  176. }
  177. };
  178. }
  179. /**
  180. * @ngdoc directive
  181. * @name mdSidenav
  182. * @module material.components.sidenav
  183. * @restrict E
  184. *
  185. * @description
  186. *
  187. * A Sidenav component that can be opened and closed programatically.
  188. *
  189. * By default, upon opening it will slide out on top of the main content area.
  190. *
  191. * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
  192. * It can be overridden with the `md-autofocus` directive on the child element you want focused.
  193. *
  194. * @usage
  195. * <hljs lang="html">
  196. * <div layout="row" ng-controller="MyController">
  197. * <md-sidenav md-component-id="left" class="md-sidenav-left">
  198. * Left Nav!
  199. * </md-sidenav>
  200. *
  201. * <md-content>
  202. * Center Content
  203. * <md-button ng-click="openLeftMenu()">
  204. * Open Left Menu
  205. * </md-button>
  206. * </md-content>
  207. *
  208. * <md-sidenav md-component-id="right"
  209. * md-is-locked-open="$mdMedia('min-width: 333px')"
  210. * class="md-sidenav-right">
  211. * <form>
  212. * <md-input-container>
  213. * <label for="testInput">Test input</label>
  214. * <input id="testInput" type="text"
  215. * ng-model="data" md-autofocus>
  216. * </md-input-container>
  217. * </form>
  218. * </md-sidenav>
  219. * </div>
  220. * </hljs>
  221. *
  222. * <hljs lang="js">
  223. * var app = angular.module('myApp', ['ngMaterial']);
  224. * app.controller('MyController', function($scope, $mdSidenav) {
  225. * $scope.openLeftMenu = function() {
  226. * $mdSidenav('left').toggle();
  227. * };
  228. * });
  229. * </hljs>
  230. *
  231. * @param {expression=} md-is-open A model bound to whether the sidenav is opened.
  232. * @param {boolean=} md-disable-backdrop When present in the markup, the sidenav will not show a backdrop.
  233. * @param {string=} md-component-id componentId to use with $mdSidenav service.
  234. * @param {expression=} md-is-locked-open When this expression evaluates to true,
  235. * the sidenav 'locks open': it falls into the content's flow instead
  236. * of appearing over it. This overrides the `md-is-open` attribute.
  237. * @param {string=} md-disable-scroll-target Selector, pointing to an element, whose scrolling will
  238. * be disabled when the sidenav is opened. By default this is the sidenav's direct parent.
  239. *
  240. * The $mdMedia() service is exposed to the is-locked-open attribute, which
  241. * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets.
  242. * Examples:
  243. *
  244. * - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
  245. * - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
  246. * - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
  247. */
  248. function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
  249. return {
  250. restrict: 'E',
  251. scope: {
  252. isOpen: '=?mdIsOpen'
  253. },
  254. controller: '$mdSidenavController',
  255. compile: function(element) {
  256. element.addClass('md-closed');
  257. element.attr('tabIndex', '-1');
  258. return postLink;
  259. }
  260. };
  261. /**
  262. * Directive Post Link function...
  263. */
  264. function postLink(scope, element, attr, sidenavCtrl) {
  265. var lastParentOverFlow;
  266. var backdrop;
  267. var disableScrollTarget = null;
  268. var triggeringElement = null;
  269. var previousContainerStyles;
  270. var promise = $q.when(true);
  271. var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
  272. var isLocked = function() {
  273. return isLockedOpenParsed(scope.$parent, {
  274. $media: function(arg) {
  275. $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead.");
  276. return $mdMedia(arg);
  277. },
  278. $mdMedia: $mdMedia
  279. });
  280. };
  281. if (attr.mdDisableScrollTarget) {
  282. disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget);
  283. if (disableScrollTarget) {
  284. disableScrollTarget = angular.element(disableScrollTarget);
  285. } else {
  286. $log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' +
  287. 'selector "{selector}". Falling back to parent.', { selector: attr.mdDisableScrollTarget }));
  288. }
  289. }
  290. if (!disableScrollTarget) {
  291. disableScrollTarget = element.parent();
  292. }
  293. // Only create the backdrop if the backdrop isn't disabled.
  294. if (!attr.hasOwnProperty('mdDisableBackdrop')) {
  295. backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
  296. }
  297. element.addClass('_md'); // private md component indicator for styling
  298. $mdTheming(element);
  299. // The backdrop should inherit the sidenavs theme,
  300. // because the backdrop will take its parent theme by default.
  301. if ( backdrop ) $mdTheming.inherit(backdrop, element);
  302. element.on('$destroy', function() {
  303. backdrop && backdrop.remove();
  304. sidenavCtrl.destroy();
  305. });
  306. scope.$on('$destroy', function(){
  307. backdrop && backdrop.remove();
  308. });
  309. scope.$watch(isLocked, updateIsLocked);
  310. scope.$watch('isOpen', updateIsOpen);
  311. // Publish special accessor for the Controller instance
  312. sidenavCtrl.$toggleOpen = toggleOpen;
  313. /**
  314. * Toggle the DOM classes to indicate `locked`
  315. * @param isLocked
  316. */
  317. function updateIsLocked(isLocked, oldValue) {
  318. scope.isLockedOpen = isLocked;
  319. if (isLocked === oldValue) {
  320. element.toggleClass('md-locked-open', !!isLocked);
  321. } else {
  322. $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
  323. }
  324. if (backdrop) {
  325. backdrop.toggleClass('md-locked-open', !!isLocked);
  326. }
  327. }
  328. /**
  329. * Toggle the SideNav view and attach/detach listeners
  330. * @param isOpen
  331. */
  332. function updateIsOpen(isOpen) {
  333. // Support deprecated md-sidenav-focus attribute as fallback
  334. var focusEl = $mdUtil.findFocusTarget(element) || $mdUtil.findFocusTarget(element,'[md-sidenav-focus]') || element;
  335. var parent = element.parent();
  336. parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
  337. if (backdrop) backdrop[isOpen ? 'on' : 'off']('click', close);
  338. var restorePositioning = updateContainerPositions(parent, isOpen);
  339. if ( isOpen ) {
  340. // Capture upon opening..
  341. triggeringElement = $document[0].activeElement;
  342. }
  343. disableParentScroll(isOpen);
  344. return promise = $q.all([
  345. isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
  346. $animate.leave(backdrop) : $q.when(true),
  347. $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
  348. ]).then(function() {
  349. // Perform focus when animations are ALL done...
  350. if (scope.isOpen) {
  351. focusEl && focusEl.focus();
  352. }
  353. // Restores the positioning on the sidenav and backdrop.
  354. restorePositioning && restorePositioning();
  355. });
  356. }
  357. function updateContainerPositions(parent, willOpen) {
  358. var drawerEl = element[0];
  359. var scrollTop = parent[0].scrollTop;
  360. if (willOpen && scrollTop) {
  361. previousContainerStyles = {
  362. top: drawerEl.style.top,
  363. bottom: drawerEl.style.bottom,
  364. height: drawerEl.style.height
  365. };
  366. // When the parent is scrolled down, then we want to be able to show the sidenav at the current scroll
  367. // position. We're moving the sidenav down to the correct scroll position and apply the height of the
  368. // parent, to increase the performance. Using 100% as height, will impact the performance heavily.
  369. var positionStyle = {
  370. top: scrollTop + 'px',
  371. bottom: 'auto',
  372. height: parent[0].clientHeight + 'px'
  373. };
  374. // Apply the new position styles to the sidenav and backdrop.
  375. element.css(positionStyle);
  376. backdrop.css(positionStyle);
  377. }
  378. // When the sidenav is closing and we have previous defined container styles,
  379. // then we return a restore function, which resets the sidenav and backdrop.
  380. if (!willOpen && previousContainerStyles) {
  381. return function() {
  382. drawerEl.style.top = previousContainerStyles.top;
  383. drawerEl.style.bottom = previousContainerStyles.bottom;
  384. drawerEl.style.height = previousContainerStyles.height;
  385. backdrop[0].style.top = null;
  386. backdrop[0].style.bottom = null;
  387. backdrop[0].style.height = null;
  388. previousContainerStyles = null;
  389. };
  390. }
  391. }
  392. /**
  393. * Prevent parent scrolling (when the SideNav is open)
  394. */
  395. function disableParentScroll(disabled) {
  396. if ( disabled && !lastParentOverFlow ) {
  397. lastParentOverFlow = disableScrollTarget.css('overflow');
  398. disableScrollTarget.css('overflow', 'hidden');
  399. } else if (angular.isDefined(lastParentOverFlow)) {
  400. disableScrollTarget.css('overflow', lastParentOverFlow);
  401. lastParentOverFlow = undefined;
  402. }
  403. }
  404. /**
  405. * Toggle the sideNav view and publish a promise to be resolved when
  406. * the view animation finishes.
  407. *
  408. * @param isOpen
  409. * @returns {*}
  410. */
  411. function toggleOpen( isOpen ) {
  412. if (scope.isOpen == isOpen ) {
  413. return $q.when(true);
  414. } else {
  415. if (scope.isOpen && sidenavCtrl.onCloseCb) sidenavCtrl.onCloseCb();
  416. return $q(function(resolve){
  417. // Toggle value to force an async `updateIsOpen()` to run
  418. scope.isOpen = isOpen;
  419. $mdUtil.nextTick(function() {
  420. // When the current `updateIsOpen()` animation finishes
  421. promise.then(function(result) {
  422. if ( !scope.isOpen ) {
  423. // reset focus to originating element (if available) upon close
  424. triggeringElement && triggeringElement.focus();
  425. triggeringElement = null;
  426. }
  427. resolve(result);
  428. });
  429. });
  430. });
  431. }
  432. }
  433. /**
  434. * Auto-close sideNav when the `escape` key is pressed.
  435. * @param evt
  436. */
  437. function onKeyDown(ev) {
  438. var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
  439. return isEscape ? close(ev) : $q.when(true);
  440. }
  441. /**
  442. * With backdrop `clicks` or `escape` key-press, immediately
  443. * apply the CSS close transition... Then notify the controller
  444. * to close() and perform its own actions.
  445. */
  446. function close(ev) {
  447. ev.preventDefault();
  448. return sidenavCtrl.close();
  449. }
  450. }
  451. }
  452. /*
  453. * @private
  454. * @ngdoc controller
  455. * @name SidenavController
  456. * @module material.components.sidenav
  457. *
  458. */
  459. function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) {
  460. var self = this;
  461. // Use Default internal method until overridden by directive postLink
  462. // Synchronous getters
  463. self.isOpen = function() { return !!$scope.isOpen; };
  464. self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
  465. // Synchronous setters
  466. self.onClose = function (callback) {
  467. self.onCloseCb = callback;
  468. return self;
  469. };
  470. // Async actions
  471. self.open = function() { return self.$toggleOpen( true ); };
  472. self.close = function() { return self.$toggleOpen( false ); };
  473. self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); };
  474. self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); };
  475. self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId);
  476. }
  477. ngmaterial.components.sidenav = angular.module("material.components.sidenav");