/*!
|
|
* 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 `<md-radio-group>` directive identifies a grouping
|
|
* container for the 1..n grouped radio buttons; specified using nested
|
|
* `<md-radio-button>` 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: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently
|
|
* than the native `<input type='radio'>` controls. Whereas the native controls
|
|
* force the user to tab through all the radio buttons, `<md-radio-group>`
|
|
* is focusable, and by default the `<md-radio-button>`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
|
|
* <hljs lang="html">
|
|
* <md-radio-group ng-model="selected">
|
|
*
|
|
* <md-radio-button
|
|
* ng-repeat="d in colorOptions"
|
|
* ng-value="d.value" aria-label="{{ d.label }}">
|
|
*
|
|
* {{ d.label }}
|
|
*
|
|
* </md-radio-button>
|
|
*
|
|
* </md-radio-group>
|
|
* </hljs>
|
|
*
|
|
*/
|
|
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 `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
|
|
*
|
|
* While similar to the `<input type="radio" ng-model="" value="">` directive,
|
|
* the `<md-radio-button>` 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
|
|
* <hljs lang="html">
|
|
*
|
|
* <md-radio-button value="1" aria-label="Label 1">
|
|
* Label 1
|
|
* </md-radio-button>
|
|
*
|
|
* <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
|
|
* Green
|
|
* </md-radio-button>
|
|
*
|
|
* </hljs>
|
|
*
|
|
*/
|
|
function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
|
|
|
|
var CHECKED_CSS = 'md-checked';
|
|
|
|
return {
|
|
restrict: 'E',
|
|
require: '^mdRadioGroup',
|
|
transclude: true,
|
|
template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
|
|
'<div class="md-off"></div>' +
|
|
'<div class="md-on"></div>' +
|
|
'</div>' +
|
|
'<div ng-transclude class="md-label"></div>',
|
|
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");
|