1 | /*!
|
---|
2 | * AngularJS Material Design
|
---|
3 | * https://github.com/angular/material
|
---|
4 | * @license MIT
|
---|
5 | * v1.2.3
|
---|
6 | */
|
---|
7 | (function( window, angular, undefined ){
|
---|
8 | "use strict";
|
---|
9 |
|
---|
10 | /**
|
---|
11 | * @ngdoc module
|
---|
12 | * @name material.components.checkbox
|
---|
13 | * @description Checkbox module!
|
---|
14 | */
|
---|
15 | MdCheckboxDirective['$inject'] = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$mdInteraction"];
|
---|
16 | angular
|
---|
17 | .module('material.components.checkbox', ['material.core'])
|
---|
18 | .directive('mdCheckbox', MdCheckboxDirective);
|
---|
19 |
|
---|
20 | /**
|
---|
21 | * @ngdoc directive
|
---|
22 | * @name mdCheckbox
|
---|
23 | * @module material.components.checkbox
|
---|
24 | * @restrict E
|
---|
25 | *
|
---|
26 | * @description
|
---|
27 | * The checkbox directive is used like the normal
|
---|
28 | * [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
|
---|
29 | *
|
---|
30 | * As per the [Material Design spec](https://material.io/archive/guidelines/style/color.html#color-color-palette)
|
---|
31 | * the checkbox is in the accent color by default. The primary color palette may be used with
|
---|
32 | * the `md-primary` class.
|
---|
33 | *
|
---|
34 | * @param {expression} ng-model Assignable angular expression to data-bind to.
|
---|
35 | * @param {string=} name Property name of the form under which the control is published.
|
---|
36 | * @param {expression=} ng-true-value The value to which the expression should be set when selected.
|
---|
37 | * @param {expression=} ng-false-value The value to which the expression should be set when not
|
---|
38 | * selected.
|
---|
39 | * @param {expression=} ng-change Expression to be executed when the model value changes.
|
---|
40 | * @param {boolean=} md-no-ink If present, disable ink ripple effects.
|
---|
41 | * @param {string=} aria-label Adds label to checkbox for accessibility.
|
---|
42 | * Defaults to checkbox's text. If no default text is found, a warning will be logged.
|
---|
43 | * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as
|
---|
44 | * 'indeterminate'. If a truthy expression or no value is passed in the checkbox renders in the
|
---|
45 | * md-indeterminate state. If falsy expression is passed in it just looks like a normal unchecked
|
---|
46 | * checkbox. The indeterminate, checked, and unchecked states are mutually exclusive. A box
|
---|
47 | * cannot be in any two states at the same time. Adding the 'md-indeterminate' attribute
|
---|
48 | * overrides any checked/unchecked rendering logic. When using the 'md-indeterminate' attribute
|
---|
49 | * use 'ng-checked' to define rendering logic instead of using 'ng-model'.
|
---|
50 | * @param {expression=} ng-checked If this expression evaluates as truthy, the 'md-checked' css
|
---|
51 | * class is added to the checkbox and it will appear checked.
|
---|
52 | *
|
---|
53 | * @usage
|
---|
54 | * <hljs lang="html">
|
---|
55 | * <md-checkbox ng-model="isChecked" aria-label="Finished?">
|
---|
56 | * Finished ?
|
---|
57 | * </md-checkbox>
|
---|
58 | *
|
---|
59 | * <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
|
---|
60 | * No Ink Effects
|
---|
61 | * </md-checkbox>
|
---|
62 | *
|
---|
63 | * <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
|
---|
64 | * Disabled
|
---|
65 | * </md-checkbox>
|
---|
66 | *
|
---|
67 | * </hljs>
|
---|
68 | *
|
---|
69 | */
|
---|
70 | function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $mdInteraction) {
|
---|
71 | inputDirective = inputDirective[0];
|
---|
72 |
|
---|
73 | return {
|
---|
74 | restrict: 'E',
|
---|
75 | transclude: true,
|
---|
76 | require: ['^?mdInputContainer', '?ngModel', '?^form'],
|
---|
77 | priority: $mdConstant.BEFORE_NG_ARIA,
|
---|
78 | template:
|
---|
79 | '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
|
---|
80 | '<div class="md-icon"></div>' +
|
---|
81 | '</div>' +
|
---|
82 | '<div ng-transclude class="md-label"></div>',
|
---|
83 | compile: compile
|
---|
84 | };
|
---|
85 |
|
---|
86 | // **********************************************************
|
---|
87 | // Private Methods
|
---|
88 | // **********************************************************
|
---|
89 |
|
---|
90 | function compile (tElement, tAttrs) {
|
---|
91 | tAttrs.$set('tabindex', tAttrs.tabindex || '0');
|
---|
92 | tAttrs.$set('type', 'checkbox');
|
---|
93 | tAttrs.$set('role', tAttrs.type);
|
---|
94 | tElement.addClass('md-auto-horizontal-margin');
|
---|
95 |
|
---|
96 | return {
|
---|
97 | pre: function(scope, element) {
|
---|
98 | // Attach a click handler during preLink, in order to immediately stop propagation
|
---|
99 | // (especially for ng-click) when the checkbox is disabled.
|
---|
100 | element.on('click', function(e) {
|
---|
101 | if (this.hasAttribute('disabled')) {
|
---|
102 | e.stopImmediatePropagation();
|
---|
103 | }
|
---|
104 | });
|
---|
105 | },
|
---|
106 | post: postLink
|
---|
107 | };
|
---|
108 |
|
---|
109 | function postLink(scope, element, attr, ctrls) {
|
---|
110 | var isIndeterminate;
|
---|
111 | var containerCtrl = ctrls[0];
|
---|
112 | var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
|
---|
113 | var formCtrl = ctrls[2];
|
---|
114 | var labelHasLink = element.find('a').length > 0;
|
---|
115 |
|
---|
116 | // The original component structure is not accessible when the checkbox's label contains a link.
|
---|
117 | // In order to keep backwards compatibility, we're only changing the structure of the component
|
---|
118 | // when we detect a link within the label. Using a span after the md-checkbox and attaching it
|
---|
119 | // via aria-labelledby allows screen readers to find and work with the link within the label.
|
---|
120 | if (labelHasLink) {
|
---|
121 | var labelId = 'label-' + $mdUtil.nextUid();
|
---|
122 | attr.$set('aria-labelledby', labelId);
|
---|
123 |
|
---|
124 | var label = element.children()[1];
|
---|
125 | // Use jQLite here since ChildNode.remove() is not supported in IE11.
|
---|
126 | angular.element(label).remove();
|
---|
127 | label.removeAttribute('ng-transclude');
|
---|
128 | label.className = 'md-checkbox-link-label';
|
---|
129 | label.setAttribute('id', labelId);
|
---|
130 | element.after(label);
|
---|
131 | // Make sure that clicking on the label still causes the checkbox to be toggled, when appropriate.
|
---|
132 | var externalLabel = element.next();
|
---|
133 | externalLabel.on('click', listener);
|
---|
134 | }
|
---|
135 |
|
---|
136 | if (containerCtrl) {
|
---|
137 | var isErrorGetter = containerCtrl.isErrorGetter || function() {
|
---|
138 | return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
|
---|
139 | };
|
---|
140 |
|
---|
141 | containerCtrl.input = element;
|
---|
142 |
|
---|
143 | scope.$watch(isErrorGetter, containerCtrl.setInvalid);
|
---|
144 | }
|
---|
145 |
|
---|
146 | $mdTheming(element);
|
---|
147 |
|
---|
148 | // Redirect focus events to the root element, because IE11 is always focusing the container element instead
|
---|
149 | // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur`
|
---|
150 | element.children().on('focus', function() {
|
---|
151 | element.focus();
|
---|
152 | });
|
---|
153 |
|
---|
154 | if ($mdUtil.parseAttributeBoolean(attr.mdIndeterminate)) {
|
---|
155 | setIndeterminateState();
|
---|
156 | scope.$watch(attr.mdIndeterminate, setIndeterminateState);
|
---|
157 | }
|
---|
158 |
|
---|
159 | if (attr.ngChecked) {
|
---|
160 | scope.$watch(scope.$eval.bind(scope, attr.ngChecked), function(value) {
|
---|
161 | ngModelCtrl.$setViewValue(value);
|
---|
162 | ngModelCtrl.$render();
|
---|
163 | });
|
---|
164 | }
|
---|
165 |
|
---|
166 | $$watchExpr('ngDisabled', 'tabindex', {
|
---|
167 | true: '-1',
|
---|
168 | false: attr.tabindex
|
---|
169 | });
|
---|
170 |
|
---|
171 | // Don't emit a warning when the label has a link within it. In that case we'll use
|
---|
172 | // aria-labelledby to point to another span that should be read as the label.
|
---|
173 | if (!labelHasLink) {
|
---|
174 | $mdAria.expectWithText(element, 'aria-label');
|
---|
175 | }
|
---|
176 |
|
---|
177 | // Reuse the original input[type=checkbox] directive from AngularJS core.
|
---|
178 | // This is a bit hacky as we need our own event listener and own render
|
---|
179 | // function.
|
---|
180 | inputDirective.link.pre(scope, {
|
---|
181 | on: angular.noop,
|
---|
182 | 0: {}
|
---|
183 | }, attr, [ngModelCtrl]);
|
---|
184 |
|
---|
185 | element.on('click', listener)
|
---|
186 | .on('keypress', keypressHandler)
|
---|
187 | .on('focus', function() {
|
---|
188 | if ($mdInteraction.getLastInteractionType() === 'keyboard') {
|
---|
189 | element.addClass('md-focused');
|
---|
190 | }
|
---|
191 | })
|
---|
192 | .on('blur', function() {
|
---|
193 | element.removeClass('md-focused');
|
---|
194 | });
|
---|
195 |
|
---|
196 | ngModelCtrl.$render = render;
|
---|
197 |
|
---|
198 | function $$watchExpr(expr, htmlAttr, valueOpts) {
|
---|
199 | if (attr[expr]) {
|
---|
200 | scope.$watch(attr[expr], function(val) {
|
---|
201 | if (valueOpts[val]) {
|
---|
202 | element.attr(htmlAttr, valueOpts[val]);
|
---|
203 | }
|
---|
204 | });
|
---|
205 | }
|
---|
206 | }
|
---|
207 |
|
---|
208 | /**
|
---|
209 | * @param {KeyboardEvent} ev 'keypress' event to handle
|
---|
210 | */
|
---|
211 | function keypressHandler(ev) {
|
---|
212 | var keyCode = ev.which || ev.keyCode;
|
---|
213 | var submit, form;
|
---|
214 |
|
---|
215 | ev.preventDefault();
|
---|
216 | switch (keyCode) {
|
---|
217 | case $mdConstant.KEY_CODE.SPACE:
|
---|
218 | element.addClass('md-focused');
|
---|
219 | listener(ev);
|
---|
220 | break;
|
---|
221 | case $mdConstant.KEY_CODE.ENTER:
|
---|
222 | // Match the behavior of the native <input type="checkbox">.
|
---|
223 | // When the enter key is pressed while focusing a native checkbox inside a form,
|
---|
224 | // the browser will trigger a `click` on the first non-disabled submit button/input
|
---|
225 | // in the form. Note that this is different from text inputs, which
|
---|
226 | // will directly submit the form without needing a submit button/input to be present.
|
---|
227 | form = $mdUtil.getClosest(ev.target, 'form');
|
---|
228 | if (form) {
|
---|
229 | submit = form.querySelector('button[type="submit"]:enabled, input[type="submit"]:enabled');
|
---|
230 | if (submit) {
|
---|
231 | submit.click();
|
---|
232 | }
|
---|
233 | }
|
---|
234 | break;
|
---|
235 | }
|
---|
236 | }
|
---|
237 |
|
---|
238 | function listener(ev) {
|
---|
239 | // skipToggle boolean is used by the switch directive to prevent the click event
|
---|
240 | // when releasing the drag. There will be always a click if releasing the drag over the checkbox.
|
---|
241 | // If the click came from a link in the checkbox, don't toggle the value.
|
---|
242 | // We want the link to be opened without changing the value in this case.
|
---|
243 | if (element[0].hasAttribute('disabled') || scope.skipToggle || ev.target.tagName === 'A') {
|
---|
244 | return;
|
---|
245 | }
|
---|
246 |
|
---|
247 | scope.$apply(function() {
|
---|
248 | // Toggle the checkbox value...
|
---|
249 | var viewValue = attr.ngChecked && attr.ngClick ? attr.checked : !ngModelCtrl.$viewValue;
|
---|
250 |
|
---|
251 | ngModelCtrl.$setViewValue(viewValue, ev && ev.type);
|
---|
252 | ngModelCtrl.$render();
|
---|
253 | });
|
---|
254 | }
|
---|
255 |
|
---|
256 | function render() {
|
---|
257 | // Cast the $viewValue to a boolean since it could be undefined
|
---|
258 | var checked = !!ngModelCtrl.$viewValue && !isIndeterminate;
|
---|
259 | element.toggleClass('md-checked', checked);
|
---|
260 | if (!isIndeterminate) {
|
---|
261 | if (checked) {
|
---|
262 | element.attr('aria-checked', 'true');
|
---|
263 | } else {
|
---|
264 | element.attr('aria-checked', 'false');
|
---|
265 | }
|
---|
266 | }
|
---|
267 | }
|
---|
268 |
|
---|
269 | /**
|
---|
270 | * @param {string=} newValue
|
---|
271 | */
|
---|
272 | function setIndeterminateState(newValue) {
|
---|
273 | isIndeterminate = newValue !== false;
|
---|
274 | if (isIndeterminate) {
|
---|
275 | element.attr('aria-checked', 'mixed');
|
---|
276 | }
|
---|
277 | element.toggleClass('md-indeterminate', isIndeterminate);
|
---|
278 | ngModelCtrl.$render();
|
---|
279 | }
|
---|
280 | }
|
---|
281 | }
|
---|
282 | }
|
---|
283 |
|
---|
284 | })(window, window.angular); |
---|