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.

574 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. goog.provide('ngmaterial.components.fabShared');
  8. goog.require('ngmaterial.core');
  9. (function() {
  10. 'use strict';
  11. MdFabController['$inject'] = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"];
  12. angular.module('material.components.fabShared', ['material.core'])
  13. .controller('MdFabController', MdFabController);
  14. function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) {
  15. var vm = this;
  16. var initialAnimationAttempts = 0;
  17. // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops
  18. vm.open = function() {
  19. $scope.$evalAsync("vm.isOpen = true");
  20. };
  21. vm.close = function() {
  22. // Async eval to avoid conflicts with existing digest loops
  23. $scope.$evalAsync("vm.isOpen = false");
  24. // Focus the trigger when the element closes so users can still tab to the next item
  25. $element.find('md-fab-trigger')[0].focus();
  26. };
  27. // Toggle the open/close state when the trigger is clicked
  28. vm.toggle = function() {
  29. $scope.$evalAsync("vm.isOpen = !vm.isOpen");
  30. };
  31. /*
  32. * Angular Lifecycle hook for newer Angular versions.
  33. * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
  34. */
  35. vm.$onInit = function() {
  36. setupDefaults();
  37. setupListeners();
  38. setupWatchers();
  39. fireInitialAnimations();
  40. };
  41. // For Angular 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
  42. // manually call the $onInit hook.
  43. if (angular.version.major === 1 && angular.version.minor <= 4) {
  44. this.$onInit();
  45. }
  46. function setupDefaults() {
  47. // Set the default direction to 'down' if none is specified
  48. vm.direction = vm.direction || 'down';
  49. // Set the default to be closed
  50. vm.isOpen = vm.isOpen || false;
  51. // Start the keyboard interaction at the first action
  52. resetActionIndex();
  53. // Add an animations waiting class so we know not to run
  54. $element.addClass('md-animations-waiting');
  55. }
  56. function setupListeners() {
  57. var eventTypes = [
  58. 'click', 'focusin', 'focusout'
  59. ];
  60. // Add our listeners
  61. angular.forEach(eventTypes, function(eventType) {
  62. $element.on(eventType, parseEvents);
  63. });
  64. // Remove our listeners when destroyed
  65. $scope.$on('$destroy', function() {
  66. angular.forEach(eventTypes, function(eventType) {
  67. $element.off(eventType, parseEvents);
  68. });
  69. // remove any attached keyboard handlers in case element is removed while
  70. // speed dial is open
  71. disableKeyboard();
  72. });
  73. }
  74. var closeTimeout;
  75. function parseEvents(event) {
  76. // If the event is a click, just handle it
  77. if (event.type == 'click') {
  78. handleItemClick(event);
  79. }
  80. // If we focusout, set a timeout to close the element
  81. if (event.type == 'focusout' && !closeTimeout) {
  82. closeTimeout = $timeout(function() {
  83. vm.close();
  84. }, 100, false);
  85. }
  86. // If we see a focusin and there is a timeout about to run, cancel it so we stay open
  87. if (event.type == 'focusin' && closeTimeout) {
  88. $timeout.cancel(closeTimeout);
  89. closeTimeout = null;
  90. }
  91. }
  92. function resetActionIndex() {
  93. vm.currentActionIndex = -1;
  94. }
  95. function setupWatchers() {
  96. // Watch for changes to the direction and update classes/attributes
  97. $scope.$watch('vm.direction', function(newDir, oldDir) {
  98. // Add the appropriate classes so we can target the direction in the CSS
  99. $animate.removeClass($element, 'md-' + oldDir);
  100. $animate.addClass($element, 'md-' + newDir);
  101. // Reset the action index since it may have changed
  102. resetActionIndex();
  103. });
  104. var trigger, actions;
  105. // Watch for changes to md-open
  106. $scope.$watch('vm.isOpen', function(isOpen) {
  107. // Reset the action index since it may have changed
  108. resetActionIndex();
  109. // We can't get the trigger/actions outside of the watch because the component hasn't been
  110. // linked yet, so we wait until the first watch fires to cache them.
  111. if (!trigger || !actions) {
  112. trigger = getTriggerElement();
  113. actions = getActionsElement();
  114. }
  115. if (isOpen) {
  116. enableKeyboard();
  117. } else {
  118. disableKeyboard();
  119. }
  120. var toAdd = isOpen ? 'md-is-open' : '';
  121. var toRemove = isOpen ? '' : 'md-is-open';
  122. // Set the proper ARIA attributes
  123. trigger.attr('aria-haspopup', true);
  124. trigger.attr('aria-expanded', isOpen);
  125. actions.attr('aria-hidden', !isOpen);
  126. // Animate the CSS classes
  127. $animate.setClass($element, toAdd, toRemove);
  128. });
  129. }
  130. function fireInitialAnimations() {
  131. // If the element is actually visible on the screen
  132. if ($element[0].scrollHeight > 0) {
  133. // Fire our animation
  134. $animate.addClass($element, '_md-animations-ready').then(function() {
  135. // Remove the waiting class
  136. $element.removeClass('md-animations-waiting');
  137. });
  138. }
  139. // Otherwise, try for up to 1 second before giving up
  140. else if (initialAnimationAttempts < 10) {
  141. $timeout(fireInitialAnimations, 100);
  142. // Increment our counter
  143. initialAnimationAttempts = initialAnimationAttempts + 1;
  144. }
  145. }
  146. function enableKeyboard() {
  147. $element.on('keydown', keyPressed);
  148. // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid
  149. // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button)
  150. $mdUtil.nextTick(function() {
  151. angular.element(document).on('click touchend', checkForOutsideClick);
  152. });
  153. // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but
  154. // this breaks accessibility, especially on mobile, since you have no arrow keys to press
  155. //resetActionTabIndexes();
  156. }
  157. function disableKeyboard() {
  158. $element.off('keydown', keyPressed);
  159. angular.element(document).off('click touchend', checkForOutsideClick);
  160. }
  161. function checkForOutsideClick(event) {
  162. if (event.target) {
  163. var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger');
  164. var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions');
  165. if (!closestTrigger && !closestActions) {
  166. vm.close();
  167. }
  168. }
  169. }
  170. function keyPressed(event) {
  171. switch (event.which) {
  172. case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false;
  173. case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false;
  174. case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false;
  175. case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false;
  176. case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false;
  177. }
  178. }
  179. function doActionPrev(event) {
  180. focusAction(event, -1);
  181. }
  182. function doActionNext(event) {
  183. focusAction(event, 1);
  184. }
  185. function focusAction(event, direction) {
  186. var actions = resetActionTabIndexes();
  187. // Increment/decrement the counter with restrictions
  188. vm.currentActionIndex = vm.currentActionIndex + direction;
  189. vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex);
  190. vm.currentActionIndex = Math.max(0, vm.currentActionIndex);
  191. // Focus the element
  192. var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0];
  193. angular.element(focusElement).attr('tabindex', 0);
  194. focusElement.focus();
  195. // Make sure the event doesn't bubble and cause something else
  196. event.preventDefault();
  197. event.stopImmediatePropagation();
  198. }
  199. function resetActionTabIndexes() {
  200. // Grab all of the actions
  201. var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
  202. // Disable all other actions for tabbing
  203. angular.forEach(actions, function(action) {
  204. angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
  205. });
  206. return actions;
  207. }
  208. function doKeyLeft(event) {
  209. if (vm.direction === 'left') {
  210. doActionNext(event);
  211. } else {
  212. doActionPrev(event);
  213. }
  214. }
  215. function doKeyUp(event) {
  216. if (vm.direction === 'down') {
  217. doActionPrev(event);
  218. } else {
  219. doActionNext(event);
  220. }
  221. }
  222. function doKeyRight(event) {
  223. if (vm.direction === 'left') {
  224. doActionPrev(event);
  225. } else {
  226. doActionNext(event);
  227. }
  228. }
  229. function doKeyDown(event) {
  230. if (vm.direction === 'up') {
  231. doActionPrev(event);
  232. } else {
  233. doActionNext(event);
  234. }
  235. }
  236. function isTrigger(element) {
  237. return $mdUtil.getClosest(element, 'md-fab-trigger');
  238. }
  239. function isAction(element) {
  240. return $mdUtil.getClosest(element, 'md-fab-actions');
  241. }
  242. function handleItemClick(event) {
  243. if (isTrigger(event.target)) {
  244. vm.toggle();
  245. }
  246. if (isAction(event.target)) {
  247. vm.close();
  248. }
  249. }
  250. function getTriggerElement() {
  251. return $element.find('md-fab-trigger');
  252. }
  253. function getActionsElement() {
  254. return $element.find('md-fab-actions');
  255. }
  256. }
  257. })();
  258. (function() {
  259. 'use strict';
  260. /**
  261. * The duration of the CSS animation in milliseconds.
  262. *
  263. * @type {number}
  264. */
  265. MdFabSpeedDialFlingAnimation['$inject'] = ["$timeout"];
  266. MdFabSpeedDialScaleAnimation['$inject'] = ["$timeout"];
  267. var cssAnimationDuration = 300;
  268. /**
  269. * @ngdoc module
  270. * @name material.components.fabSpeedDial
  271. */
  272. angular
  273. // Declare our module
  274. .module('material.components.fabSpeedDial', [
  275. 'material.core',
  276. 'material.components.fabShared',
  277. 'material.components.fabActions'
  278. ])
  279. // Register our directive
  280. .directive('mdFabSpeedDial', MdFabSpeedDialDirective)
  281. // Register our custom animations
  282. .animation('.md-fling', MdFabSpeedDialFlingAnimation)
  283. .animation('.md-scale', MdFabSpeedDialScaleAnimation)
  284. // Register a service for each animation so that we can easily inject them into unit tests
  285. .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation)
  286. .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation);
  287. /**
  288. * @ngdoc directive
  289. * @name mdFabSpeedDial
  290. * @module material.components.fabSpeedDial
  291. *
  292. * @restrict E
  293. *
  294. * @description
  295. * The `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually
  296. * `<md-button>`s) for quick access to common actions.
  297. *
  298. * There are currently two animations available by applying one of the following classes to
  299. * the component:
  300. *
  301. * - `md-fling` - The speed dial items appear from underneath the trigger and move into their
  302. * appropriate positions.
  303. * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%.
  304. *
  305. * You may also easily position the trigger by applying one one of the following classes to the
  306. * `<md-fab-speed-dial>` element:
  307. * - `md-fab-top-left`
  308. * - `md-fab-top-right`
  309. * - `md-fab-bottom-left`
  310. * - `md-fab-bottom-right`
  311. *
  312. * These CSS classes use `position: absolute`, so you need to ensure that the container element
  313. * also uses `position: absolute` or `position: relative` in order for them to work.
  314. *
  315. * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to
  316. * open or close the speed dial. However, if you wish to allow users to hover over the empty
  317. * space where the actions will appear, you must also add the `md-hover-full` class to the speed
  318. * dial element. Without this, the hover effect will only occur on top of the trigger.
  319. *
  320. * See the demos for more information.
  321. *
  322. * ## Troubleshooting
  323. *
  324. * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on
  325. * the parent container to ensure that it is only visible once ready. We have plans to remove this
  326. * necessity in the future.
  327. *
  328. * @usage
  329. * <hljs lang="html">
  330. * <md-fab-speed-dial md-direction="up" class="md-fling">
  331. * <md-fab-trigger>
  332. * <md-button aria-label="Add..."><md-icon md-svg-src="/img/icons/plus.svg"></md-icon></md-button>
  333. * </md-fab-trigger>
  334. *
  335. * <md-fab-actions>
  336. * <md-button aria-label="Add User">
  337. * <md-icon md-svg-src="/img/icons/user.svg"></md-icon>
  338. * </md-button>
  339. *
  340. * <md-button aria-label="Add Group">
  341. * <md-icon md-svg-src="/img/icons/group.svg"></md-icon>
  342. * </md-button>
  343. * </md-fab-actions>
  344. * </md-fab-speed-dial>
  345. * </hljs>
  346. *
  347. * @param {string} md-direction From which direction you would like the speed dial to appear
  348. * relative to the trigger element.
  349. * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible.
  350. */
  351. function MdFabSpeedDialDirective() {
  352. return {
  353. restrict: 'E',
  354. scope: {
  355. direction: '@?mdDirection',
  356. isOpen: '=?mdOpen'
  357. },
  358. bindToController: true,
  359. controller: 'MdFabController',
  360. controllerAs: 'vm',
  361. link: FabSpeedDialLink
  362. };
  363. function FabSpeedDialLink(scope, element) {
  364. // Prepend an element to hold our CSS variables so we can use them in the animations below
  365. element.prepend('<div class="_md-css-variables"></div>');
  366. }
  367. }
  368. function MdFabSpeedDialFlingAnimation($timeout) {
  369. function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
  370. function runAnimation(element) {
  371. // Don't run if we are still waiting and we are not ready
  372. if (element.hasClass('md-animations-waiting') && !element.hasClass('_md-animations-ready')) {
  373. return;
  374. }
  375. var el = element[0];
  376. var ctrl = element.controller('mdFabSpeedDial');
  377. var items = el.querySelectorAll('.md-fab-action-item');
  378. // Grab our trigger element
  379. var triggerElement = el.querySelector('md-fab-trigger');
  380. // Grab our element which stores CSS variables
  381. var variablesElement = el.querySelector('._md-css-variables');
  382. // Setup JS variables based on our CSS variables
  383. var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
  384. // Always reset the items to their natural position/state
  385. angular.forEach(items, function(item, index) {
  386. var styles = item.style;
  387. styles.transform = styles.webkitTransform = '';
  388. styles.transitionDelay = '';
  389. styles.opacity = 1;
  390. // Make the items closest to the trigger have the highest z-index
  391. styles.zIndex = (items.length - index) + startZIndex;
  392. });
  393. // Set the trigger to be above all of the actions so they disappear behind it.
  394. triggerElement.style.zIndex = startZIndex + items.length + 1;
  395. // If the control is closed, hide the items behind the trigger
  396. if (!ctrl.isOpen) {
  397. angular.forEach(items, function(item, index) {
  398. var newPosition, axis;
  399. var styles = item.style;
  400. // Make sure to account for differences in the dimensions of the trigger verses the items
  401. // so that we can properly center everything; this helps hide the item's shadows behind
  402. // the trigger.
  403. var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2;
  404. var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2;
  405. switch (ctrl.direction) {
  406. case 'up':
  407. newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset);
  408. axis = 'Y';
  409. break;
  410. case 'down':
  411. newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset);
  412. axis = 'Y';
  413. break;
  414. case 'left':
  415. newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset);
  416. axis = 'X';
  417. break;
  418. case 'right':
  419. newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset);
  420. axis = 'X';
  421. break;
  422. }
  423. var newTranslate = 'translate' + axis + '(' + newPosition + 'px)';
  424. styles.transform = styles.webkitTransform = newTranslate;
  425. });
  426. }
  427. }
  428. return {
  429. addClass: function(element, className, done) {
  430. if (element.hasClass('md-fling')) {
  431. runAnimation(element);
  432. delayDone(done);
  433. } else {
  434. done();
  435. }
  436. },
  437. removeClass: function(element, className, done) {
  438. runAnimation(element);
  439. delayDone(done);
  440. }
  441. };
  442. }
  443. function MdFabSpeedDialScaleAnimation($timeout) {
  444. function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
  445. var delay = 65;
  446. function runAnimation(element) {
  447. var el = element[0];
  448. var ctrl = element.controller('mdFabSpeedDial');
  449. var items = el.querySelectorAll('.md-fab-action-item');
  450. // Grab our element which stores CSS variables
  451. var variablesElement = el.querySelector('._md-css-variables');
  452. // Setup JS variables based on our CSS variables
  453. var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
  454. // Always reset the items to their natural position/state
  455. angular.forEach(items, function(item, index) {
  456. var styles = item.style,
  457. offsetDelay = index * delay;
  458. styles.opacity = ctrl.isOpen ? 1 : 0;
  459. styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)';
  460. styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms';
  461. // Make the items closest to the trigger have the highest z-index
  462. styles.zIndex = (items.length - index) + startZIndex;
  463. });
  464. }
  465. return {
  466. addClass: function(element, className, done) {
  467. runAnimation(element);
  468. delayDone(done);
  469. },
  470. removeClass: function(element, className, done) {
  471. runAnimation(element);
  472. delayDone(done);
  473. }
  474. };
  475. }
  476. })();
  477. ngmaterial.components.fabShared = angular.module("material.components.fabShared");