1 | /*!
|
---|
2 | * AngularJS Material Design
|
---|
3 | * https://github.com/angular/material
|
---|
4 | * @license MIT
|
---|
5 | * v1.2.3
|
---|
6 | */
|
---|
7 | goog.provide('ngmaterial.components.radioButton');
|
---|
8 | goog.require('ngmaterial.core');
|
---|
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 | /**
|
---|
23 | * @type {Readonly<{NEXT: number, CURRENT: number, PREVIOUS: number}>}
|
---|
24 | */
|
---|
25 | var incrementSelection = Object.freeze({PREVIOUS: -1, CURRENT: 0, NEXT: 1});
|
---|
26 |
|
---|
27 | /**
|
---|
28 | * @ngdoc directive
|
---|
29 | * @module material.components.radioButton
|
---|
30 | * @name mdRadioGroup
|
---|
31 | *
|
---|
32 | * @restrict E
|
---|
33 | *
|
---|
34 | * @description
|
---|
35 | * The `<md-radio-group>` directive identifies a grouping
|
---|
36 | * container for the 1..n grouped radio buttons; specified using nested
|
---|
37 | * `<md-radio-button>` elements.
|
---|
38 | *
|
---|
39 | * The radio button uses the accent color by default. The primary color palette may be used with
|
---|
40 | * the `md-primary` class.
|
---|
41 | *
|
---|
42 | * Note: `<md-radio-group>` and `<md-radio-button>` handle `tabindex` differently
|
---|
43 | * than the native `<input type="radio">` controls. Whereas the native controls
|
---|
44 | * force the user to tab through all the radio buttons, `<md-radio-group>`
|
---|
45 | * is focusable and by default the `<md-radio-button>`s are not.
|
---|
46 | *
|
---|
47 | * @param {string} ng-model Assignable angular expression to data-bind to.
|
---|
48 | * @param {string=} ng-change AngularJS expression to be executed when input changes due to user
|
---|
49 | * interaction.
|
---|
50 | * @param {boolean=} md-no-ink If present, disables ink ripple effects.
|
---|
51 | *
|
---|
52 | * @usage
|
---|
53 | * <hljs lang="html">
|
---|
54 | * <md-radio-group ng-model="selected">
|
---|
55 | * <md-radio-button ng-repeat="item in items"
|
---|
56 | * ng-value="item.value" aria-label="{{item.label}}">
|
---|
57 | * {{ item.label }}
|
---|
58 | * </md-radio-button>
|
---|
59 | * </md-radio-group>
|
---|
60 | * </hljs>
|
---|
61 | */
|
---|
62 | function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) {
|
---|
63 | RadioGroupController.prototype = createRadioGroupControllerProto();
|
---|
64 |
|
---|
65 | return {
|
---|
66 | restrict: 'E',
|
---|
67 | controller: ['$element', RadioGroupController],
|
---|
68 | require: ['mdRadioGroup', '?ngModel'],
|
---|
69 | link: { pre: linkRadioGroup }
|
---|
70 | };
|
---|
71 |
|
---|
72 | function linkRadioGroup(scope, element, attr, controllers) {
|
---|
73 | // private md component indicator for styling
|
---|
74 | element.addClass('_md');
|
---|
75 | $mdTheming(element);
|
---|
76 |
|
---|
77 | var radioGroupController = controllers[0];
|
---|
78 | var ngModelCtrl = controllers[1] || $mdUtil.fakeNgModel();
|
---|
79 |
|
---|
80 | radioGroupController.init(ngModelCtrl);
|
---|
81 |
|
---|
82 | scope.mouseActive = false;
|
---|
83 |
|
---|
84 | element
|
---|
85 | .attr({
|
---|
86 | 'role': 'radiogroup',
|
---|
87 | 'tabIndex': element.attr('tabindex') || '0'
|
---|
88 | })
|
---|
89 | .on('keydown', keydownListener)
|
---|
90 | .on('mousedown', function() {
|
---|
91 | scope.mouseActive = true;
|
---|
92 | $timeout(function() {
|
---|
93 | scope.mouseActive = false;
|
---|
94 | }, 100);
|
---|
95 | })
|
---|
96 | .on('focus', function() {
|
---|
97 | if (scope.mouseActive === false) {
|
---|
98 | radioGroupController.$element.addClass('md-focused');
|
---|
99 | }
|
---|
100 | })
|
---|
101 | .on('blur', function() {
|
---|
102 | radioGroupController.$element.removeClass('md-focused');
|
---|
103 | });
|
---|
104 |
|
---|
105 | // Initially set the first radio button as the aria-activedescendant. This will be overridden
|
---|
106 | // if a 'checked' radio button gets rendered. We need to wait for the nextTick here so that the
|
---|
107 | // radio buttons have their id values assigned.
|
---|
108 | $mdUtil.nextTick(function () {
|
---|
109 | var radioButtons = getRadioButtons(radioGroupController.$element);
|
---|
110 | if (radioButtons.count() &&
|
---|
111 | !radioGroupController.$element[0].hasAttribute('aria-activedescendant')) {
|
---|
112 | radioGroupController.setActiveDescendant(radioButtons.first().id);
|
---|
113 | }
|
---|
114 | });
|
---|
115 |
|
---|
116 | /**
|
---|
117 | * Apply the md-focused class if it isn't already applied.
|
---|
118 | */
|
---|
119 | function setFocus() {
|
---|
120 | if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
|
---|
121 | }
|
---|
122 |
|
---|
123 | /**
|
---|
124 | * @param {KeyboardEvent} keyboardEvent
|
---|
125 | */
|
---|
126 | function keydownListener(keyboardEvent) {
|
---|
127 | var keyCode = keyboardEvent.which || keyboardEvent.keyCode;
|
---|
128 |
|
---|
129 | // Only listen to events that we originated ourselves
|
---|
130 | // so that we don't trigger on things like arrow keys in inputs.
|
---|
131 | if (keyCode !== $mdConstant.KEY_CODE.ENTER &&
|
---|
132 | keyboardEvent.currentTarget !== keyboardEvent.target) {
|
---|
133 | return;
|
---|
134 | }
|
---|
135 |
|
---|
136 | switch (keyCode) {
|
---|
137 | case $mdConstant.KEY_CODE.LEFT_ARROW:
|
---|
138 | case $mdConstant.KEY_CODE.UP_ARROW:
|
---|
139 | keyboardEvent.preventDefault();
|
---|
140 | radioGroupController.selectPrevious();
|
---|
141 | setFocus();
|
---|
142 | break;
|
---|
143 |
|
---|
144 | case $mdConstant.KEY_CODE.RIGHT_ARROW:
|
---|
145 | case $mdConstant.KEY_CODE.DOWN_ARROW:
|
---|
146 | keyboardEvent.preventDefault();
|
---|
147 | radioGroupController.selectNext();
|
---|
148 | setFocus();
|
---|
149 | break;
|
---|
150 |
|
---|
151 | case $mdConstant.KEY_CODE.SPACE:
|
---|
152 | keyboardEvent.preventDefault();
|
---|
153 | radioGroupController.selectCurrent();
|
---|
154 | break;
|
---|
155 |
|
---|
156 | case $mdConstant.KEY_CODE.ENTER:
|
---|
157 | var form = angular.element($mdUtil.getClosest(element[0], 'form'));
|
---|
158 | if (form.length > 0) {
|
---|
159 | form.triggerHandler('submit');
|
---|
160 | }
|
---|
161 | break;
|
---|
162 | }
|
---|
163 | }
|
---|
164 | }
|
---|
165 |
|
---|
166 | /**
|
---|
167 | * @param {JQLite} $element
|
---|
168 | * @constructor
|
---|
169 | */
|
---|
170 | function RadioGroupController($element) {
|
---|
171 | this._radioButtonRenderFns = [];
|
---|
172 | this.$element = $element;
|
---|
173 | }
|
---|
174 |
|
---|
175 | function createRadioGroupControllerProto() {
|
---|
176 | return {
|
---|
177 | init: function(ngModelCtrl) {
|
---|
178 | this._ngModelCtrl = ngModelCtrl;
|
---|
179 | this._ngModelCtrl.$render = angular.bind(this, this.render);
|
---|
180 | },
|
---|
181 | add: function(rbRender) {
|
---|
182 | this._radioButtonRenderFns.push(rbRender);
|
---|
183 | },
|
---|
184 | remove: function(rbRender) {
|
---|
185 | var index = this._radioButtonRenderFns.indexOf(rbRender);
|
---|
186 | if (index !== -1) {
|
---|
187 | this._radioButtonRenderFns.splice(index, 1);
|
---|
188 | }
|
---|
189 | },
|
---|
190 | render: function() {
|
---|
191 | this._radioButtonRenderFns.forEach(function(rbRender) {
|
---|
192 | rbRender();
|
---|
193 | });
|
---|
194 | },
|
---|
195 | setViewValue: function(value, eventType) {
|
---|
196 | this._ngModelCtrl.$setViewValue(value, eventType);
|
---|
197 | // update the other radio buttons as well
|
---|
198 | this.render();
|
---|
199 | },
|
---|
200 | getViewValue: function() {
|
---|
201 | return this._ngModelCtrl.$viewValue;
|
---|
202 | },
|
---|
203 | selectCurrent: function() {
|
---|
204 | return changeSelectedButton(this.$element, incrementSelection.CURRENT);
|
---|
205 | },
|
---|
206 | selectNext: function() {
|
---|
207 | return changeSelectedButton(this.$element, incrementSelection.NEXT);
|
---|
208 | },
|
---|
209 | selectPrevious: function() {
|
---|
210 | return changeSelectedButton(this.$element, incrementSelection.PREVIOUS);
|
---|
211 | },
|
---|
212 | setActiveDescendant: function (radioId) {
|
---|
213 | this.$element.attr('aria-activedescendant', radioId);
|
---|
214 | },
|
---|
215 | isDisabled: function() {
|
---|
216 | return this.$element[0].hasAttribute('disabled');
|
---|
217 | }
|
---|
218 | };
|
---|
219 | }
|
---|
220 |
|
---|
221 | /**
|
---|
222 | * Coerce all child radio buttons into an array, then wrap them in an iterator.
|
---|
223 | * @param parent {!JQLite}
|
---|
224 | * @return {{add: add, next: (function()), last: (function(): any|null), previous: (function()), count: (function(): number), hasNext: (function(*=): Array.length|*|number|boolean), inRange: (function(*): boolean), remove: remove, contains: (function(*=): *|boolean), itemAt: (function(*=): any|null), findBy: (function(*, *): *[]), hasPrevious: (function(*=): Array.length|*|number|boolean), items: (function(): *[]), indexOf: (function(*=): number), first: (function(): any|null)}}
|
---|
225 | */
|
---|
226 | function getRadioButtons(parent) {
|
---|
227 | return $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
|
---|
228 | }
|
---|
229 |
|
---|
230 | /**
|
---|
231 | * Change the radio group's selected button by a given increment.
|
---|
232 | * If no button is selected, select the first button.
|
---|
233 | * @param {JQLite} parent the md-radio-group
|
---|
234 | * @param {incrementSelection} increment enum that determines whether the next or
|
---|
235 | * previous button is clicked. For current, only the first button is selected, otherwise the
|
---|
236 | * current selection is maintained (by doing nothing).
|
---|
237 | */
|
---|
238 | function changeSelectedButton(parent, increment) {
|
---|
239 | var buttons = getRadioButtons(parent);
|
---|
240 | var target;
|
---|
241 |
|
---|
242 | if (buttons.count()) {
|
---|
243 | var validate = function (button) {
|
---|
244 | // If disabled, then NOT valid
|
---|
245 | return !angular.element(button).attr("disabled");
|
---|
246 | };
|
---|
247 |
|
---|
248 | var selected = parent[0].querySelector('md-radio-button.md-checked');
|
---|
249 | if (!selected) {
|
---|
250 | target = buttons.first();
|
---|
251 | } else if (increment === incrementSelection.PREVIOUS ||
|
---|
252 | increment === incrementSelection.NEXT) {
|
---|
253 | target = buttons[
|
---|
254 | increment === incrementSelection.PREVIOUS ? 'previous' : 'next'
|
---|
255 | ](selected, validate);
|
---|
256 | }
|
---|
257 |
|
---|
258 | if (target) {
|
---|
259 | // Activate radioButton's click listener (triggerHandler won't create a real click event)
|
---|
260 | angular.element(target).triggerHandler('click');
|
---|
261 | }
|
---|
262 | }
|
---|
263 | }
|
---|
264 | }
|
---|
265 |
|
---|
266 | /**
|
---|
267 | * @ngdoc directive
|
---|
268 | * @module material.components.radioButton
|
---|
269 | * @name mdRadioButton
|
---|
270 | *
|
---|
271 | * @restrict E
|
---|
272 | *
|
---|
273 | * @description
|
---|
274 | * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
|
---|
275 | *
|
---|
276 | * While similar to the `<input type="radio" ng-model="" value="">` directive,
|
---|
277 | * the `<md-radio-button>` directive provides ink effects, ARIA support, and
|
---|
278 | * supports use within named radio groups.
|
---|
279 | *
|
---|
280 | * One of `value` or `ng-value` must be set so that the `md-radio-group`'s model is set properly when the
|
---|
281 | * `md-radio-button` is selected.
|
---|
282 | *
|
---|
283 | * @param {string} value The value to which the model should be set when selected.
|
---|
284 | * @param {string} ng-value AngularJS expression which sets the value to which the model should
|
---|
285 | * be set when selected.
|
---|
286 | * @param {string=} name Property name of the form under which the control is published.
|
---|
287 | * @param {string=} aria-label Adds label to radio button for accessibility.
|
---|
288 | * Defaults to radio button's text. If no text content is available, a warning will be logged.
|
---|
289 | *
|
---|
290 | * @usage
|
---|
291 | * <hljs lang="html">
|
---|
292 | *
|
---|
293 | * <md-radio-button value="1" aria-label="Label 1">
|
---|
294 | * Label 1
|
---|
295 | * </md-radio-button>
|
---|
296 | *
|
---|
297 | * <md-radio-button ng-value="specialValue" aria-label="Green">
|
---|
298 | * Green
|
---|
299 | * </md-radio-button>
|
---|
300 | *
|
---|
301 | * </hljs>
|
---|
302 | *
|
---|
303 | */
|
---|
304 | function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
|
---|
305 |
|
---|
306 | var CHECKED_CSS = 'md-checked';
|
---|
307 |
|
---|
308 | return {
|
---|
309 | restrict: 'E',
|
---|
310 | require: '^mdRadioGroup',
|
---|
311 | transclude: true,
|
---|
312 | template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
|
---|
313 | '<div class="md-off"></div>' +
|
---|
314 | '<div class="md-on"></div>' +
|
---|
315 | '</div>' +
|
---|
316 | '<div ng-transclude class="md-label"></div>',
|
---|
317 | link: link
|
---|
318 | };
|
---|
319 |
|
---|
320 | function link(scope, element, attr, radioGroupController) {
|
---|
321 | var lastChecked;
|
---|
322 |
|
---|
323 | $mdTheming(element);
|
---|
324 | configureAria(element);
|
---|
325 | element.addClass('md-auto-horizontal-margin');
|
---|
326 |
|
---|
327 | // ngAria overwrites the aria-checked inside a $watch for ngValue.
|
---|
328 | // We should defer the initialization until all the watches have fired.
|
---|
329 | // This can also be fixed by removing the `lastChecked` check, but that'll
|
---|
330 | // cause more DOM manipulation on each digest.
|
---|
331 | if (attr.ngValue) {
|
---|
332 | $mdUtil.nextTick(initialize, false);
|
---|
333 | } else {
|
---|
334 | initialize();
|
---|
335 | }
|
---|
336 |
|
---|
337 | /**
|
---|
338 | * Initializes the component.
|
---|
339 | */
|
---|
340 | function initialize() {
|
---|
341 | if (!radioGroupController) {
|
---|
342 | throw 'RadioButton: No RadioGroupController could be found.';
|
---|
343 | }
|
---|
344 |
|
---|
345 | radioGroupController.add(render);
|
---|
346 | attr.$observe('value', render);
|
---|
347 |
|
---|
348 | element
|
---|
349 | .on('click', listener)
|
---|
350 | .on('$destroy', function() {
|
---|
351 | radioGroupController.remove(render);
|
---|
352 | });
|
---|
353 | }
|
---|
354 |
|
---|
355 | /**
|
---|
356 | * On click functionality.
|
---|
357 | */
|
---|
358 | function listener(ev) {
|
---|
359 | if (element[0].hasAttribute('disabled') || radioGroupController.isDisabled()) return;
|
---|
360 |
|
---|
361 | scope.$apply(function() {
|
---|
362 | radioGroupController.setViewValue(attr.value, ev && ev.type);
|
---|
363 | });
|
---|
364 | }
|
---|
365 |
|
---|
366 | /**
|
---|
367 | * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent).
|
---|
368 | * Update the `aria-activedescendant` attribute.
|
---|
369 | */
|
---|
370 | function render() {
|
---|
371 | var checked = radioGroupController.getViewValue() == attr.value;
|
---|
372 |
|
---|
373 | if (checked === lastChecked) return;
|
---|
374 |
|
---|
375 | if (element[0] && element[0].parentNode &&
|
---|
376 | element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') {
|
---|
377 | // If the radioButton is inside a div, then add class so highlighting will work.
|
---|
378 | element.parent().toggleClass(CHECKED_CSS, checked);
|
---|
379 | }
|
---|
380 |
|
---|
381 | if (checked) {
|
---|
382 | radioGroupController.setActiveDescendant(element.attr('id'));
|
---|
383 | }
|
---|
384 |
|
---|
385 | lastChecked = checked;
|
---|
386 |
|
---|
387 | element
|
---|
388 | .attr('aria-checked', checked)
|
---|
389 | .toggleClass(CHECKED_CSS, checked);
|
---|
390 | }
|
---|
391 |
|
---|
392 | /**
|
---|
393 | * Inject ARIA-specific attributes appropriate for each radio button
|
---|
394 | */
|
---|
395 | function configureAria(element) {
|
---|
396 | element.attr({
|
---|
397 | id: attr.id || 'radio_' + $mdUtil.nextUid(),
|
---|
398 | role: 'radio',
|
---|
399 | 'aria-checked': 'false'
|
---|
400 | });
|
---|
401 |
|
---|
402 | $mdAria.expectWithText(element, 'aria-label');
|
---|
403 | }
|
---|
404 | }
|
---|
405 | }
|
---|
406 |
|
---|
407 | ngmaterial.components.radioButton = angular.module("material.components.radioButton"); |
---|