diff --git a/src/components/checkbox/checkbox-theme.scss b/src/components/checkbox/checkbox-theme.scss index afe6ebabab6..d9389e9871e 100644 --- a/src/components/checkbox/checkbox-theme.scss +++ b/src/components/checkbox/checkbox-theme.scss @@ -30,6 +30,12 @@ &#{$checkedSelector} ._md-icon:after { border-color: '{{primary-contrast-0.87}}'; } + + & [indeterminate][disabled] { + ._md-container { + color: '{{foreground-3}}'; + } + } } md-checkbox.md-THEME_NAME-theme { diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js index 6b2e0f1ffa1..a30eebb96c7 100644 --- a/src/components/checkbox/checkbox.js +++ b/src/components/checkbox/checkbox.js @@ -27,6 +27,19 @@ angular * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element. * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects * @param {string=} aria-label Adds label to checkbox for accessibility. + * @param {boolean=} indeterminate Specifies that this checkbox can be rendered + * indeterminate. If true is passed in the checkbox renders in the indeterminate + * state. If no value is passed in this defaults to false. To use the other + * indeterminate-* attributes this attribute must be present. + * @param {expression=} indeterminate-when This determines when the checkbox + * should switch from an indeterminate to non-indeterminate state. + * @param {expression=} indeterminate-checked-when This determines when the + * indeterminate checkbox should appear as just a normal check box. If the + * checkbox has indeterminate="false" and is not checked, it appears as just a + * normal unchecked checkbox. + * @param {expression=} indeterminate-click This gets run when the + * indeterminate checkbox is clicked. This allows the user to trigger a function + * when an indeterminate checkbox is clicked. * Defaults to checkbox's text. If no default text is found, a warning will be logged. * * @usage @@ -69,6 +82,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ function compile (tElement, tAttrs) { var container = tElement.children(); + var indeterminateStateEnabled = tElement[0].hasAttribute('indeterminate'); tAttrs.type = 'checkbox'; tAttrs.tabindex = tAttrs.tabindex || '0'; @@ -91,6 +105,13 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ return function postLink(scope, element, attr, ngModelCtrl) { ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel(); $mdTheming(element); + if (indeterminateStateEnabled) { + // Bootstrap initial state + setIndeterminateState(); + if (attr.indeterminateWhen) { + scope.$watch(attr.indeterminateWhen, setIndeterminateState); + } + } if (attr.ngChecked) { scope.$watch( @@ -156,11 +177,18 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ listener(ev); } } + function listener(ev) { if (element[0].hasAttribute('disabled')) { return; } + if (indeterminateStateEnabled) { + if (attr.indeterminateClick) { + scope.$apply(element.attr('indeterminate-click')); + } + } + scope.$apply(function() { // Toggle the checkbox value... var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue; @@ -171,12 +199,48 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ } function render() { + if (indeterminateStateEnabled) { + setIndeterminateState(); + // The checkbox should never be in an indeterminate="true" AND checked state. + // To prevent this, if the checkbox is in an indetermiante + // state, we skip the code below. + return; + } + if(ngModelCtrl.$viewValue) { element.addClass(CHECKED_CSS); } else { element.removeClass(CHECKED_CSS); } } + + function setIndeterminateState() { + var isIndeterminate = element.attr('indeterminate') === 'true' || false; + + if (attr.indeterminateWhen) { + isIndeterminate = scope.$eval(attr.indeterminateWhen); + } + + // We never want a checkbox to be checked AND indeterminate, they are + // mutually exclusive states. + if (attr.indeterminateCheckedWhen) { + var isChecked = scope.$eval(attr.indeterminateCheckedWhen); + if (isChecked) { + element.addClass(CHECKED_CSS); + isIndeterminate = false; + } else { + element.removeClass(CHECKED_CSS); + } + } + + if (isIndeterminate) { + element[0].indeterminate = true; + } else { + element[0].indeterminate = undefined; + } + + element.attr('indeterminate', isIndeterminate); + } }; } } diff --git a/src/components/checkbox/checkbox.scss b/src/components/checkbox/checkbox.scss index 11978ba1301..a64316b6959 100644 --- a/src/components/checkbox/checkbox.scss +++ b/src/components/checkbox/checkbox.scss @@ -105,6 +105,24 @@ $checkbox-top: 12px !default; cursor: default; } + &[indeterminate="true"] ._md-icon{ + &:after { + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: table; + width: $width * 0.6; + height: $border-width; + border-width: $border-width; + border-style: solid; + border-top: 0; + border-left: 0; + content: ''; + } + } + } .md-inline-form { diff --git a/src/components/checkbox/checkbox.spec.js b/src/components/checkbox/checkbox.spec.js index 5a2ab924433..3b718f51be2 100644 --- a/src/components/checkbox/checkbox.spec.js +++ b/src/components/checkbox/checkbox.spec.js @@ -246,5 +246,116 @@ describe('mdCheckbox', function() { expect(isChecked(checkbox)).toBe(false); expect(checkbox.hasClass('ng-invalid')).toBe(true); }); + + + describe('with the indeterminate attribute', function() { + + it('should set indeterminate attr to false by default', function() { + var element = compileAndLink( + '
' + + '' + + '
'); + + var checkbox = element.find('md-checkbox'); + + pageScope.$apply(); + + expect(checkbox.attr('indeterminate')).toBe("false"); + }); + + it('should set its HTMLInputElement.indeterminate to be undefined when not indeterminate', function() { + var element = compileAndLink( + '
' + + '' + + '
'); + + var checkbox = element.find('md-checkbox'); + + pageScope.$apply(); + + expect(checkbox[0].indeterminate).toBe(undefined); + }); + + it('should set its HTMLInputElement.indeterminate to be "true" when indeterminate', function() { + var element = compileAndLink( + '
' + + '' + + '
'); + + var checkbox = element.find('md-checkbox'); + + pageScope.$apply(); + + expect(checkbox[0].indeterminate).toBe(true); + }); + + it('should be set indeterminate attr value according to indeterminate-when attr', function() { + + pageScope.isIndeterminate = function() { return true; } ; + + var element = compileAndLink( + '
' + + '' + + '
'); + + var checkbox = element.find('md-checkbox'); + + pageScope.$apply(); + + expect(checkbox.attr('indeterminate')).toBe("true"); + }); + + it('should be set checkbox checked value according to indeterminate-checked-when attr', function() { + + pageScope.isChecked = function() { return true; } ; + + var element = compileAndLink( + '
' + + '' + + '
'); + + var checkbox = element.find('md-checkbox'); + + pageScope.$apply(); + + expect(checkbox.hasClass(CHECKED_CSS)).toEqual(true); + }); + + it('should call indeterminate-click function passed in when indeterminate checkbox is clicked', function() { + var clickIndet = jasmine.createSpy('clickIndet'); + + pageScope.clickIndet = clickIndet; + + var element = compileAndLink( + '
' + + '' + + '
'); + + var checkbox = element.find('md-checkbox'); + + checkbox.triggerHandler('click'); + + pageScope.$apply(); + + expect(clickIndet).toHaveBeenCalled(); + }); + + it('should never be indeterminate="true" and "md-checked" at the same time', function() { + pageScope.isChecked = function() { return true; }; + + var element = compileAndLink( + '
' + + '' + + '
'); + + var checkbox = element.find('md-checkbox'); + + pageScope.$apply(); + expect(checkbox.attr('indeterminate')).toBe("false"); + expect(checkbox[0].indeterminate).toBe(undefined); + expect(checkbox.hasClass(CHECKED_CSS)).toEqual(true); + }); + + }); }); }); diff --git a/src/components/checkbox/demoBasicUsage/index.html b/src/components/checkbox/demoBasicUsage/index.html index 35f6a1043d9..054190def0c 100644 --- a/src/components/checkbox/demoBasicUsage/index.html +++ b/src/components/checkbox/demoBasicUsage/index.html @@ -43,6 +43,18 @@ Checkbox (md-primary): No Ink +
+ + Checkbox: Indeterminate + +
+
+ + Checkbox: Disabled, Indeterminate + +
diff --git a/src/components/checkbox/demoSelectAll/index.html b/src/components/checkbox/demoSelectAll/index.html new file mode 100644 index 00000000000..9b826c356d8 --- /dev/null +++ b/src/components/checkbox/demoSelectAll/index.html @@ -0,0 +1,32 @@ +
+
+ +
+
+ +
+ Using <md-checkbox> with the 'indeterminate' attribute +
+
+ + Un-Select All + +
+
+ + {{ item }} + +
+
+
+
+
+ +
diff --git a/src/components/checkbox/demoSelectAll/script.js b/src/components/checkbox/demoSelectAll/script.js new file mode 100644 index 00000000000..00b16ce77d4 --- /dev/null +++ b/src/components/checkbox/demoSelectAll/script.js @@ -0,0 +1,41 @@ + +angular.module('checkboxDemo3', ['ngMaterial']) + +.controller('AppCtrl', function($scope) { + $scope.items = [1,2,3,4,5]; + $scope.selected = [1]; + $scope.toggle = function (item, list) { + var idx = list.indexOf(item); + if (idx > -1) list.splice(idx, 1); + else list.push(item); + }; + + $scope.exists = function (item, list) { + return list.indexOf(item) > -1; + }; + + $scope.isIndeterminate = function() { + if ($scope.selected.length !== 0 && + $scope.selected.length !== $scope.items.length) { + return true; + } else { + return false; + } + }; + + $scope.isChecked = function() { + if ($scope.selected.length === $scope.items.length) { + return true; + } else { + return false; + } + }; + + $scope.toggleAll = function() { + if ($scope.selected.length === $scope.items.length) { + $scope.selected = []; + } else if ($scope.selected.length === 0 || $scope.selected.length > 0) { + $scope.selected = $scope.items.slice(0); + } + }; +}); diff --git a/src/components/checkbox/demoSelectAll/style.css b/src/components/checkbox/demoSelectAll/style.css new file mode 100644 index 00000000000..9e0dabbd756 --- /dev/null +++ b/src/components/checkbox/demoSelectAll/style.css @@ -0,0 +1,13 @@ +legend { + color: #3F51B5; +} + +fieldset.standard { + border-style: solid; + border-width: 1px; + height: 100%; +} + +.select-all-checkboxes { + padding-left: 30px; +}