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