sidenav.js 19 KB

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