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.

362 lines
10 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.radioButton
  12. * @description radioButton module!
  13. */
  14. mdRadioGroupDirective['$inject'] = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"];
  15. mdRadioButtonDirective['$inject'] = ["$mdAria", "$mdUtil", "$mdTheming"];
  16. angular.module('material.components.radioButton', [
  17. 'material.core'
  18. ])
  19. .directive('mdRadioGroup', mdRadioGroupDirective)
  20. .directive('mdRadioButton', mdRadioButtonDirective);
  21. /**
  22. * @ngdoc directive
  23. * @module material.components.radioButton
  24. * @name mdRadioGroup
  25. *
  26. * @restrict E
  27. *
  28. * @description
  29. * The `<md-radio-group>` directive identifies a grouping
  30. * container for the 1..n grouped radio buttons; specified using nested
  31. * `<md-radio-button>` tags.
  32. *
  33. * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
  34. * the radio button is in the accent color by default. The primary color palette may be used with
  35. * the `md-primary` class.
  36. *
  37. * Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently
  38. * than the native `<input type='radio'>` controls. Whereas the native controls
  39. * force the user to tab through all the radio buttons, `<md-radio-group>`
  40. * is focusable, and by default the `<md-radio-button>`s are not.
  41. *
  42. * @param {string} ng-model Assignable angular expression to data-bind to.
  43. * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects.
  44. *
  45. * @usage
  46. * <hljs lang="html">
  47. * <md-radio-group ng-model="selected">
  48. *
  49. * <md-radio-button
  50. * ng-repeat="d in colorOptions"
  51. * ng-value="d.value" aria-label="{{ d.label }}">
  52. *
  53. * {{ d.label }}
  54. *
  55. * </md-radio-button>
  56. *
  57. * </md-radio-group>
  58. * </hljs>
  59. *
  60. */
  61. function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) {
  62. RadioGroupController.prototype = createRadioGroupControllerProto();
  63. return {
  64. restrict: 'E',
  65. controller: ['$element', RadioGroupController],
  66. require: ['mdRadioGroup', '?ngModel'],
  67. link: { pre: linkRadioGroup }
  68. };
  69. function linkRadioGroup(scope, element, attr, ctrls) {
  70. element.addClass('_md'); // private md component indicator for styling
  71. $mdTheming(element);
  72. var rgCtrl = ctrls[0];
  73. var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
  74. rgCtrl.init(ngModelCtrl);
  75. scope.mouseActive = false;
  76. element
  77. .attr({
  78. 'role': 'radiogroup',
  79. 'tabIndex': element.attr('tabindex') || '0'
  80. })
  81. .on('keydown', keydownListener)
  82. .on('mousedown', function(event) {
  83. scope.mouseActive = true;
  84. $timeout(function() {
  85. scope.mouseActive = false;
  86. }, 100);
  87. })
  88. .on('focus', function() {
  89. if(scope.mouseActive === false) {
  90. rgCtrl.$element.addClass('md-focused');
  91. }
  92. })
  93. .on('blur', function() {
  94. rgCtrl.$element.removeClass('md-focused');
  95. });
  96. /**
  97. *
  98. */
  99. function setFocus() {
  100. if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
  101. }
  102. /**
  103. *
  104. */
  105. function keydownListener(ev) {
  106. var keyCode = ev.which || ev.keyCode;
  107. // Only listen to events that we originated ourselves
  108. // so that we don't trigger on things like arrow keys in
  109. // inputs.
  110. if (keyCode != $mdConstant.KEY_CODE.ENTER &&
  111. ev.currentTarget != ev.target) {
  112. return;
  113. }
  114. switch (keyCode) {
  115. case $mdConstant.KEY_CODE.LEFT_ARROW:
  116. case $mdConstant.KEY_CODE.UP_ARROW:
  117. ev.preventDefault();
  118. rgCtrl.selectPrevious();
  119. setFocus();
  120. break;
  121. case $mdConstant.KEY_CODE.RIGHT_ARROW:
  122. case $mdConstant.KEY_CODE.DOWN_ARROW:
  123. ev.preventDefault();
  124. rgCtrl.selectNext();
  125. setFocus();
  126. break;
  127. case $mdConstant.KEY_CODE.ENTER:
  128. var form = angular.element($mdUtil.getClosest(element[0], 'form'));
  129. if (form.length > 0) {
  130. form.triggerHandler('submit');
  131. }
  132. break;
  133. }
  134. }
  135. }
  136. function RadioGroupController($element) {
  137. this._radioButtonRenderFns = [];
  138. this.$element = $element;
  139. }
  140. function createRadioGroupControllerProto() {
  141. return {
  142. init: function(ngModelCtrl) {
  143. this._ngModelCtrl = ngModelCtrl;
  144. this._ngModelCtrl.$render = angular.bind(this, this.render);
  145. },
  146. add: function(rbRender) {
  147. this._radioButtonRenderFns.push(rbRender);
  148. },
  149. remove: function(rbRender) {
  150. var index = this._radioButtonRenderFns.indexOf(rbRender);
  151. if (index !== -1) {
  152. this._radioButtonRenderFns.splice(index, 1);
  153. }
  154. },
  155. render: function() {
  156. this._radioButtonRenderFns.forEach(function(rbRender) {
  157. rbRender();
  158. });
  159. },
  160. setViewValue: function(value, eventType) {
  161. this._ngModelCtrl.$setViewValue(value, eventType);
  162. // update the other radio buttons as well
  163. this.render();
  164. },
  165. getViewValue: function() {
  166. return this._ngModelCtrl.$viewValue;
  167. },
  168. selectNext: function() {
  169. return changeSelectedButton(this.$element, 1);
  170. },
  171. selectPrevious: function() {
  172. return changeSelectedButton(this.$element, -1);
  173. },
  174. setActiveDescendant: function (radioId) {
  175. this.$element.attr('aria-activedescendant', radioId);
  176. },
  177. isDisabled: function() {
  178. return this.$element[0].hasAttribute('disabled');
  179. }
  180. };
  181. }
  182. /**
  183. * Change the radio group's selected button by a given increment.
  184. * If no button is selected, select the first button.
  185. */
  186. function changeSelectedButton(parent, increment) {
  187. // Coerce all child radio buttons into an array, then wrap then in an iterator
  188. var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
  189. if (buttons.count()) {
  190. var validate = function (button) {
  191. // If disabled, then NOT valid
  192. return !angular.element(button).attr("disabled");
  193. };
  194. var selected = parent[0].querySelector('md-radio-button.md-checked');
  195. var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first();
  196. // Activate radioButton's click listener (triggerHandler won't create a real click event)
  197. angular.element(target).triggerHandler('click');
  198. }
  199. }
  200. }
  201. /**
  202. * @ngdoc directive
  203. * @module material.components.radioButton
  204. * @name mdRadioButton
  205. *
  206. * @restrict E
  207. *
  208. * @description
  209. * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
  210. *
  211. * While similar to the `<input type="radio" ng-model="" value="">` directive,
  212. * the `<md-radio-button>` directive provides ink effects, ARIA support, and
  213. * supports use within named radio groups.
  214. *
  215. * @param {string} ngModel Assignable angular expression to data-bind to.
  216. * @param {string=} ngChange Angular expression to be executed when input changes due to user
  217. * interaction with the input element.
  218. * @param {string} ngValue Angular expression which sets the value to which the expression should
  219. * be set when selected.
  220. * @param {string} value The value to which the expression should be set when selected.
  221. * @param {string=} name Property name of the form under which the control is published.
  222. * @param {string=} aria-label Adds label to radio button for accessibility.
  223. * Defaults to radio button's text. If no text content is available, a warning will be logged.
  224. *
  225. * @usage
  226. * <hljs lang="html">
  227. *
  228. * <md-radio-button value="1" aria-label="Label 1">
  229. * Label 1
  230. * </md-radio-button>
  231. *
  232. * <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
  233. * Green
  234. * </md-radio-button>
  235. *
  236. * </hljs>
  237. *
  238. */
  239. function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
  240. var CHECKED_CSS = 'md-checked';
  241. return {
  242. restrict: 'E',
  243. require: '^mdRadioGroup',
  244. transclude: true,
  245. template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
  246. '<div class="md-off"></div>' +
  247. '<div class="md-on"></div>' +
  248. '</div>' +
  249. '<div ng-transclude class="md-label"></div>',
  250. link: link
  251. };
  252. function link(scope, element, attr, rgCtrl) {
  253. var lastChecked;
  254. $mdTheming(element);
  255. configureAria(element, scope);
  256. // ngAria overwrites the aria-checked inside a $watch for ngValue.
  257. // We should defer the initialization until all the watches have fired.
  258. // This can also be fixed by removing the `lastChecked` check, but that'll
  259. // cause more DOM manipulation on each digest.
  260. if (attr.ngValue) {
  261. $mdUtil.nextTick(initialize, false);
  262. } else {
  263. initialize();
  264. }
  265. /**
  266. * Initializes the component.
  267. */
  268. function initialize() {
  269. if (!rgCtrl) {
  270. throw 'RadioButton: No RadioGroupController could be found.';
  271. }
  272. rgCtrl.add(render);
  273. attr.$observe('value', render);
  274. element
  275. .on('click', listener)
  276. .on('$destroy', function() {
  277. rgCtrl.remove(render);
  278. });
  279. }
  280. /**
  281. * On click functionality.
  282. */
  283. function listener(ev) {
  284. if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return;
  285. scope.$apply(function() {
  286. rgCtrl.setViewValue(attr.value, ev && ev.type);
  287. });
  288. }
  289. /**
  290. * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent).
  291. * Update the `aria-activedescendant` attribute.
  292. */
  293. function render() {
  294. var checked = rgCtrl.getViewValue() == attr.value;
  295. if (checked === lastChecked) return;
  296. if (element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') {
  297. // If the radioButton is inside a div, then add class so highlighting will work
  298. element.parent().toggleClass(CHECKED_CSS, checked);
  299. }
  300. if (checked) {
  301. rgCtrl.setActiveDescendant(element.attr('id'));
  302. }
  303. lastChecked = checked;
  304. element
  305. .attr('aria-checked', checked)
  306. .toggleClass(CHECKED_CSS, checked);
  307. }
  308. /**
  309. * Inject ARIA-specific attributes appropriate for each radio button
  310. */
  311. function configureAria(element, scope){
  312. element.attr({
  313. id: attr.id || 'radio_' + $mdUtil.nextUid(),
  314. role: 'radio',
  315. 'aria-checked': 'false'
  316. });
  317. $mdAria.expectWithText(element, 'aria-label');
  318. }
  319. }
  320. }
  321. })(window, window.angular);