diff --git a/src/bootstrap/choices.tpl.html b/src/bootstrap/choices.tpl.html index 59632c155..daddb8684 100644 --- a/src/bootstrap/choices.tpl.html +++ b/src/bootstrap/choices.tpl.html @@ -4,7 +4,7 @@
  • -
    +
  • diff --git a/src/select.css b/src/select.css index 616a0c1c4..7a18d3c10 100644 --- a/src/select.css +++ b/src/select.css @@ -136,6 +136,13 @@ background-color: #428bca; } +.ui-select-bootstrap .ui-select-choices-row.disabled>a, +.ui-select-bootstrap .ui-select-choices-row.active.disabled>a { + color: #777; + cursor: not-allowed; + background-color: #fff; +} + /* fix hide/show angular animation */ .ui-select-match.ng-hide-add, .ui-select-search.ng-hide-add { diff --git a/src/select.js b/src/select.js index 8aa5e88c8..abbdf8e22 100644 --- a/src/select.js +++ b/src/select.js @@ -156,6 +156,7 @@ ctrl.resetSearchInput = undefined; // Initialized inside uiSelect directive link function ctrl.refreshDelay = undefined; // Initialized inside uiSelectChoices directive link function ctrl.multiple = false; // Initialized inside uiSelect directive link function + ctrl.disableChoiceExpression = undefined; // Initialized inside uiSelect directive link function ctrl.isEmpty = function() { return angular.isUndefined(ctrl.selected) || ctrl.selected === null || ctrl.selected === ''; @@ -299,24 +300,40 @@ return ctrl.items.indexOf(itemScope[ctrl.itemProperty]) === ctrl.activeIndex; }; + ctrl.isDisabled = function(itemScope) { + var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]); + var isDisabled = false; + var item; + + if (itemIndex >= 0 && !angular.isUndefined(ctrl.disableChoiceExpression)) { + item = ctrl.items[itemIndex]; + isDisabled = !!(itemScope.$eval(ctrl.disableChoiceExpression)); // force the boolean value + item._uiSelectChoiceDisabled = isDisabled; // store this for later reference + } + + return isDisabled; + }; + // When the user clicks on an item inside the dropdown ctrl.select = function(item) { - var locals = {}; - locals[ctrl.parserResult.itemName] = item; + if (!item._uiSelectChoiceDisabled) { + var locals = {}; + locals[ctrl.parserResult.itemName] = item; - ctrl.onSelectCallback($scope, { - $item: item, - $model: ctrl.parserResult.modelMapper($scope, locals) - }); + ctrl.onSelectCallback($scope, { + $item: item, + $model: ctrl.parserResult.modelMapper($scope, locals) + }); - if(ctrl.multiple){ - ctrl.selected.push(item); - ctrl.sizeSearchInput(); - } else { - ctrl.selected = item; + if(ctrl.multiple){ + ctrl.selected.push(item); + ctrl.sizeSearchInput(); + } else { + ctrl.selected = item; + } + ctrl.close(); } - ctrl.close(); }; // Closes the dropdown @@ -818,6 +835,8 @@ $select.parseRepeatAttr(attrs.repeat, groupByExp); //Result ready at $select.parserResult + $select.disableChoiceExpression = attrs.uiDisableChoice; + if(groupByExp) { var groups = element.querySelectorAll('.ui-select-choices-group'); if (groups.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-group but got '{0}'.", groups.length); diff --git a/src/select2/choices.tpl.html b/src/select2/choices.tpl.html index 1fd482bc3..03821e10e 100644 --- a/src/select2/choices.tpl.html +++ b/src/select2/choices.tpl.html @@ -2,7 +2,7 @@
  • {{$group.name}}
    diff --git a/src/selectize/choices.tpl.html b/src/selectize/choices.tpl.html index 57f2c54fd..6158c7ed1 100644 --- a/src/selectize/choices.tpl.html +++ b/src/selectize/choices.tpl.html @@ -2,7 +2,7 @@
    {{$group.name}}
    -
    +
    diff --git a/test/select.spec.js b/test/select.spec.js index 7816fa452..f2f4f2690 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -266,6 +266,164 @@ describe('ui-select tests', function() { expect(getMatchLabel(el)).toEqual('false'); }); + describe('disabled options', function() { + function createUiSelect(attrs) { + var attrsDisabled = ''; + if (attrs !== undefined) { + if (attrs.disabled !== undefined) { + attrsDisabled = ' ui-disable-choice="' + attrs.disabled + '"'; + } else { + attrsDisabled = ''; + } + } + + return compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
    \ +
    \ +
    \ +
    ' + ); + } + + function disablePerson(opts) { + opts = opts || {}; + + var key = opts.key || 'people', + disableAttr = opts.disableAttr || 'disabled', + disableBool = opts.disableBool === undefined ? true : opts.disableBool, + matchAttr = opts.match || 'name', + matchVal = opts.matchVal || 'Wladimir'; + + scope['_' + key] = angular.copy(scope[key]); + scope[key].map(function (model) { + if (model[matchAttr] == matchVal) { + model[disableAttr] = disableBool; + } + return model; + }); + } + + function resetScope(opts) { + opts = opts || {}; + var key = opts.key || 'people'; + scope[key] = angular.copy(scope['_' + key]); + } + + describe('without disabling expression', function () { + beforeEach(function() { + disablePerson(); + this.el = createUiSelect(); + }); + + it('should not allow disabled options to be selected', function() { + clickItem(this.el, 'Wladimir'); + + expect(getMatchLabel(this.el)).toEqual('Wladimir'); + }); + + it('should set a disabled class on the option', function() { + var option = $(this.el).find('.ui-select-choices-row div:contains("Wladimir")'); + var container = option.closest('.ui-select-choices-row'); + + expect(container.hasClass('disabled')).toBeFalsy(); + }); + }); + + describe('disable on truthy property', function () { + beforeEach(function() { + disablePerson({ + disableAttr : 'inactive', + disableBool : true, + }); + this.el = createUiSelect({ + disabled: 'person.inactive' + }); + }); + + it('should allow the user to define the selected option', function () { + expect($(this.el).find('.ui-select-choices').attr('ui-disable-choice')).toBe('person.inactive'); + }); + + it('should not allow disabled options to be selected', function() { + clickItem(this.el, 'Wladimir'); + + expect(getMatchLabel(this.el)).not.toEqual('Wladimir'); + }); + + it('should set a disabled class on the option', function() { + var option = $(this.el).find('.ui-select-choices-row div:contains("Wladimir")'); + var container = option.closest('.ui-select-choices-row'); + + expect(container.hasClass('disabled')).toBeTruthy(); + }); + }); + + describe('disable on inverse property check', function () { + beforeEach(function() { + disablePerson({ + disableAttr : 'active', + disableBool : false, + }); + this.el = createUiSelect({ + disabled: '!person.active' + }); + }); + + it('should allow the user to define the selected option', function () { + expect($(this.el).find('.ui-select-choices').attr('ui-disable-choice')).toBe('!person.active'); + }); + + it('should not allow disabled options to be selected', function() { + clickItem(this.el, 'Wladimir'); + + expect(getMatchLabel(this.el)).not.toEqual('Wladimir'); + }); + + it('should set a disabled class on the option', function() { + var option = $(this.el).find('.ui-select-choices-row div:contains("Wladimir")'); + var container = option.closest('.ui-select-choices-row'); + + expect(container.hasClass('disabled')).toBeTruthy(); + }); + }); + + describe('disable on expression', function () { + beforeEach(function() { + disablePerson({ + disableAttr : 'status', + disableBool : 'inactive' + }); + this.el = createUiSelect({ + disabled: "person.status == 'inactive'" + }); + }); + + it('should allow the user to define the selected option', function () { + expect($(this.el).find('.ui-select-choices').attr('ui-disable-choice')).toBe("person.status == 'inactive'"); + }); + + it('should not allow disabled options to be selected', function() { + clickItem(this.el, 'Wladimir'); + + expect(getMatchLabel(this.el)).not.toEqual('Wladimir'); + }); + + it('should set a disabled class on the option', function() { + var option = $(this.el).find('.ui-select-choices-row div:contains("Wladimir")'); + var container = option.closest('.ui-select-choices-row'); + + expect(container.hasClass('disabled')).toBeTruthy(); + }); + }); + + afterEach(function() { + resetScope(); + }); + }); + describe('choices group', function() { function getGroupLabel(item) { return item.parent('.ui-select-choices-group').find('.ui-select-choices-group-label');