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.

559 lines
18 KiB

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