/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v1.1.1 */ goog.provide('ngmaterial.components.radioButton'); goog.require('ngmaterial.core'); /** * @ngdoc module * @name material.components.radioButton * @description radioButton module! */ mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; angular.module('material.components.radioButton', [ 'material.core' ]) .directive('mdRadioGroup', mdRadioGroupDirective) .directive('mdRadioButton', mdRadioButtonDirective); /** * @ngdoc directive * @module material.components.radioButton * @name mdRadioGroup * * @restrict E * * @description * The `` directive identifies a grouping * container for the 1..n grouped radio buttons; specified using nested * `` tags. * * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) * the radio button is in the accent color by default. The primary color palette may be used with * the `md-primary` class. * * Note: `` and `` handle tabindex differently * than the native `` controls. Whereas the native controls * force the user to tab through all the radio buttons, `` * is focusable, and by default the ``s are not. * * @param {string} ng-model Assignable angular expression to data-bind to. * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects. * * @usage * * * * * * {{ d.label }} * * * * * * */ function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { RadioGroupController.prototype = createRadioGroupControllerProto(); return { restrict: 'E', controller: ['$element', RadioGroupController], require: ['mdRadioGroup', '?ngModel'], link: { pre: linkRadioGroup } }; function linkRadioGroup(scope, element, attr, ctrls) { element.addClass('_md'); // private md component indicator for styling $mdTheming(element); var rgCtrl = ctrls[0]; var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); rgCtrl.init(ngModelCtrl); scope.mouseActive = false; element .attr({ 'role': 'radiogroup', 'tabIndex': element.attr('tabindex') || '0' }) .on('keydown', keydownListener) .on('mousedown', function(event) { scope.mouseActive = true; $timeout(function() { scope.mouseActive = false; }, 100); }) .on('focus', function() { if(scope.mouseActive === false) { rgCtrl.$element.addClass('md-focused'); } }) .on('blur', function() { rgCtrl.$element.removeClass('md-focused'); }); /** * */ function setFocus() { if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } } /** * */ function keydownListener(ev) { var keyCode = ev.which || ev.keyCode; // Only listen to events that we originated ourselves // so that we don't trigger on things like arrow keys in // inputs. if (keyCode != $mdConstant.KEY_CODE.ENTER && ev.currentTarget != ev.target) { return; } switch (keyCode) { case $mdConstant.KEY_CODE.LEFT_ARROW: case $mdConstant.KEY_CODE.UP_ARROW: ev.preventDefault(); rgCtrl.selectPrevious(); setFocus(); break; case $mdConstant.KEY_CODE.RIGHT_ARROW: case $mdConstant.KEY_CODE.DOWN_ARROW: ev.preventDefault(); rgCtrl.selectNext(); setFocus(); break; case $mdConstant.KEY_CODE.ENTER: var form = angular.element($mdUtil.getClosest(element[0], 'form')); if (form.length > 0) { form.triggerHandler('submit'); } break; } } } function RadioGroupController($element) { this._radioButtonRenderFns = []; this.$element = $element; } function createRadioGroupControllerProto() { return { init: function(ngModelCtrl) { this._ngModelCtrl = ngModelCtrl; this._ngModelCtrl.$render = angular.bind(this, this.render); }, add: function(rbRender) { this._radioButtonRenderFns.push(rbRender); }, remove: function(rbRender) { var index = this._radioButtonRenderFns.indexOf(rbRender); if (index !== -1) { this._radioButtonRenderFns.splice(index, 1); } }, render: function() { this._radioButtonRenderFns.forEach(function(rbRender) { rbRender(); }); }, setViewValue: function(value, eventType) { this._ngModelCtrl.$setViewValue(value, eventType); // update the other radio buttons as well this.render(); }, getViewValue: function() { return this._ngModelCtrl.$viewValue; }, selectNext: function() { return changeSelectedButton(this.$element, 1); }, selectPrevious: function() { return changeSelectedButton(this.$element, -1); }, setActiveDescendant: function (radioId) { this.$element.attr('aria-activedescendant', radioId); }, isDisabled: function() { return this.$element[0].hasAttribute('disabled'); } }; } /** * Change the radio group's selected button by a given increment. * If no button is selected, select the first button. */ function changeSelectedButton(parent, increment) { // Coerce all child radio buttons into an array, then wrap then in an iterator var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true); if (buttons.count()) { var validate = function (button) { // If disabled, then NOT valid return !angular.element(button).attr("disabled"); }; var selected = parent[0].querySelector('md-radio-button.md-checked'); var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); // Activate radioButton's click listener (triggerHandler won't create a real click event) angular.element(target).triggerHandler('click'); } } } /** * @ngdoc directive * @module material.components.radioButton * @name mdRadioButton * * @restrict E * * @description * The ``directive is the child directive required to be used within `` elements. * * While similar to the `` directive, * the `` directive provides ink effects, ARIA support, and * supports use within named radio groups. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {string} ngValue Angular expression which sets the value to which the expression should * be set when selected. * @param {string} value The value to which the expression should be set when selected. * @param {string=} name Property name of the form under which the control is published. * @param {string=} aria-label Adds label to radio button for accessibility. * Defaults to radio button's text. If no text content is available, a warning will be logged. * * @usage * * * * Label 1 * * * * Green * * * * */ function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { var CHECKED_CSS = 'md-checked'; return { restrict: 'E', require: '^mdRadioGroup', transclude: true, template: '
' + '
' + '
' + '
' + '
', link: link }; function link(scope, element, attr, rgCtrl) { var lastChecked; $mdTheming(element); configureAria(element, scope); initialize(); /** * */ function initialize() { if (!rgCtrl) { throw 'RadioButton: No RadioGroupController could be found.'; } rgCtrl.add(render); attr.$observe('value', render); element .on('click', listener) .on('$destroy', function() { rgCtrl.remove(render); }); } /** * */ function listener(ev) { if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return; scope.$apply(function() { rgCtrl.setViewValue(attr.value, ev && ev.type); }); } /** * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent). * Update the `aria-activedescendant` attribute. */ function render() { var checked = (rgCtrl.getViewValue() == attr.value); if (checked === lastChecked) { return; } lastChecked = checked; element.attr('aria-checked', checked); if (checked) { markParentAsChecked(true); element.addClass(CHECKED_CSS); rgCtrl.setActiveDescendant(element.attr('id')); } else { markParentAsChecked(false); element.removeClass(CHECKED_CSS); } /** * If the radioButton is inside a div, then add class so highlighting will work... */ function markParentAsChecked(addClass ) { if ( element.parent()[0].nodeName != "MD-RADIO-GROUP") { element.parent()[ !!addClass ? 'addClass' : 'removeClass'](CHECKED_CSS); } } } /** * Inject ARIA-specific attributes appropriate for each radio button */ function configureAria( element, scope ){ scope.ariaId = buildAriaID(); element.attr({ 'id' : scope.ariaId, 'role' : 'radio', 'aria-checked' : 'false' }); $mdAria.expectWithText(element, 'aria-label'); /** * Build a unique ID for each radio button that will be used with aria-activedescendant. * Preserve existing ID if already specified. * @returns {*|string} */ function buildAriaID() { return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() ); } } } } ngmaterial.components.radioButton = angular.module("material.components.radioButton");