radioButton.js 10 KB

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