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.

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