radioButton.js 10 KB

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