<html>

<head>
  <title>Angular Multiselect Checkbox Dropdown</title>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular-animate.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular-loader.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular-sanitize.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular-cookies.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular-touch.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular-resource.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.14.3/ui-bootstrap.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.14.3/ui-bootstrap-tpls.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.13/angular-ui-router.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui/0.4.0/angular-ui-ieshiv.min.js"></script>
  <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui/0.4.0/angular-ui.min.js"></script>
  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.css">
  <link rel="stylesheet" type="text/css" href="style.css">
  <script type='text/javascript' src="script.js"></script>
  <script type='text/javascript' src="multiselect.js"></script>
</head>

<body ng-app="myApp">
  <div ng-include="'menu.html'"></div>
  <div ui-view="main"></div>
  <script type="text/ng-template" id="menu.html">
    <nav class="navbar navbar-default">
      <div class="container-fluid">
        <div class="navbar-header">
          <span class="navbar-brand">Our Menu</span>
        </div>
      </div>
    </nav>
  </script>
  <script type="text/ng-template" id="state1.html">
    <div class="row container-fluid">
      <div class="buttons col-lg-2">
        <div class="buttons">
          <button ng-click="vm.clear()">Clear Selections</button>
          <button ng-click="vm.randomSelect()">Randomly Select</button>
        </div>
        <br/>
      </div>
      <div class="col-lg-2">
        <form>
          <div class="form-group">
            <multiselect class="input-xlarge multiselect" ng-model="vm.option1" options="p.key as p.value for p in vm.options1" select-limit="5" header="Select Stuff1" selected-header="options selected" multiple="true" required enable-filter="true" filter-placeholder="Filter stuff.."
            ng-change="vm.change('option1')">
            </multiselect>
          </div>

          <div class="form-group">
            <multiselect class="input-xlarge multiselect" ng-model="vm.option2" options="p.key as p.value for p in vm.options2" select-limit="5" header="Select Stuff2 Simple" selected-header="options selected" multiple="true" required enable-filter="true" filter-placeholder="Filter stuff.."
            ng-change="vm.change('option2')">
            </multiselect>
          </div>

          <div class="form-group">
            <multiselect class="input-xlarge multiselect" ng-model="vm.option3" options="p.key as p.value for p in vm.options2" select-limit="5" header="Select Stuff3 Complex" selected-header="options selected" multiple="true" complex-models="true" required enable-filter="true"
            filter-placeholder="Filter stuff.." ng-change="vm.change('option3')">
            </multiselect>
          </div>

          <div class="form-group">
            <multiselect class="input-xlarge multiselect" ng-model="vm.option4" options="p.key as p.value for p in vm.options1" select-limit="5" header="Single Select Stuff4" selected-header="options selected" multiple="false" required complex-models="true" enable-filter="true"
            filter-placeholder="Filter stuff.." ng-change="vm.change('option4')">
            </multiselect>
          </div>

          <div class="form-group">
            <multiselect class="input-xlarge multiselect" ng-model="vm.option5" options="p.key as p.value for p in vm.options1" select-limit="5" header="Select Stuff5 No Filter" selected-header="options selected" multiple="true" enable-filter="false" filter-placeholder="Filter stuff.."
            ng-change="vm.change('option5')">
            </multiselect>
          </div>

          <div class="form-group">
            <multiselect class="input-xlarge multiselect" ng-model="vm.option6" options="p.key as p.value for p in vm.options1" select-limit="5" header="Select Stuff6 Prepopulated" selected-header="options selected" multiple="true" enable-filter="true" filter-placeholder="Filter stuff.."
            ng-change="vm.change('option6')">
            </multiselect>
          </div>

          <div class="form-group">
            <multiselect class="input-xlarge multiselect" ng-model="vm.option7" options="p.key as p.value for p in vm.options2" select-limit="5" header="Select Stuff7 Prepopulated" selected-header="options selected" multiple="true" enable-filter="true" filter-placeholder="Filter stuff.."
            ng-change="vm.change('option7')">
            </multiselect>
          </div>
        </form>
      </div>
      <div class="row container-fluid">
        <div class="col-md-6">
          Change log
          <pre ng-bind="vm.changes | json"></pre>
        </div>
        <div class="col-md-6">
          <div>
            Option1
            <pre ng-bind="vm.option1 | json"></pre>
          </div>
          <div>
            Option2 Simple
            <pre ng-bind="vm.option2 | json"></pre>
          </div>
          <div>
            Option3 Complex
            <pre ng-bind="vm.option3 | json"></pre>
          </div>
          <div>
            Option4 Single
            <pre ng-bind="vm.option4 | json"></pre>
          </div>
          <div>
            Option5 No Filter
            <pre ng-bind="vm.option5 | json"></pre>
          </div>
          <div>
            Option6 Pre-populated with one
            <pre ng-bind="vm.option6 | json"></pre>
          </div>
          <div>
            Option7 Pre-populated with multiple
            <pre ng-bind="vm.option7 | json"></pre>
          </div>
        </div>
      </div>
    </div>
  </script>
</body>

</html>
(function () {
    angular.module('myApp.controllers', []);

    var myApp = angular.module('myApp', [
        'myApp.controllers',
        'long2know',
        'ngSanitize',
        'ui.bootstrap',
        'ui.router',
        'ui']);

    var state1Ctrl = function ($log) {
        var vm = this,
        getRandomInt = function(min, max) {
            return Math.floor(Math.random() * (max - min + 1) + min);
        };

        vm.changes = [];
        vm.options1 = [];
        for (var i = 0; i < 10; i++) {
            vm.options1.push({ key: i + 1, value: 'Prop' + (i + 1).toString() });
        }

        vm.options2 = [];
        for (var i = 0; i < 100; i++) {
            vm.options2.push({ key: i + 1, value: 'Prop' + (i + 1).toString() });
        }

        vm.option6 = 3;
        vm.option7 = [4, 11, 23];
        
        vm.clear = function() {
            vm.option1 = [];
            vm.option2 = [];
            vm.option3 = [];
            vm.option4 = [];
            vm.option5 = [];
            vm.option6 = [];
            vm.option7 = [];
            vm.changes = [];
        };
        
        vm.change = function (input) {
          var message = 'Change triggered: ' + input;
          vm.changes.push(message);
           $log.log(message);
        }
        
        vm.randomSelect = function() {
            vm.clear();
            var arrSelected = [ vm.option1, vm.option2, vm.option3, vm.option4, vm.option5, vm.option6, vm.option7];
            var arrOptions = [ vm.options1, vm.options2, vm.options2, vm.options1, vm.options1, vm.options1, vm.options2 ];
            var arrIsSingle = [ false, false, false, true, false, false, false  ];
            var arrIsSimple = [ true, true, false, false, true, true, true  ];
            
            for (var i = 0; i < arrSelected.length; i++) {
                var selected = arrSelected[i];
                var options = arrOptions[i];
                var isSingle = arrIsSingle[i];
                var isSimple = arrIsSimple[i];
                var min = 0;
                var max = options.length - 1;
                if (isSingle) {
                    var randIndex = getRandomInt(min, max);
                    if (isSimple) {
                        selected.push(options[randIndex].key); 
                    } else {
                        selected.push(options[randIndex]);
                    }
                }
                else
                {
                    var toSelectIndexes = [];
                    var numItems = getRandomInt(0, options.length) + 1;
                    for (var j = 0; j < getRandomInt(1, numItems); j++)
                    {
                        var randIndex = getRandomInt(min, max);
                        var arrIndex = toSelectIndexes.indexOf(randIndex);
                        if (arrIndex == -1) {
                            toSelectIndexes.push(randIndex);
                            if (isSimple) {
                                selected.push(options[randIndex].key); 
                            } else {
                                selected.push(options[randIndex]);   
                            }
                        }
                    }
                }
            }
        }
    };

    state1Ctrl.$inject = ['$log'];
    
    angular.module('myApp.controllers')
        .controller('state1Ctrl', state1Ctrl);

    myApp.config(['$locationProvider', '$stateProvider', '$urlRouterProvider',

        function ($locationProvider, $stateProvider, $urlRouterProvider) {

            $locationProvider.html5Mode(false);

            $urlRouterProvider.when('/', '/state1')
                .otherwise("/state1");

            $stateProvider.state('app', {
                abstract: true,
                url: '/',
                views: {
                    'main': {
                        template: '<div ui-view>/div>'
                    }
                }
            })
            .state('app.state1', {
                url: 'state1',
                templateUrl: 'state1.html',
                controller: 'state1Ctrl',
                controllerAs: 'vm',
                reloadOnSearch: false
            })
        }]);

    myApp.run(['$log', function ($log) {
        $log.log("Start.");
    }]);
})()
multiselect {
    display: block;
}

    multiselect > .btn-group {
        min-width: 180px;
    }

    multiselect .btn {
        width: 100%;
        background-color: #FFF;
    }

        multiselect .btn.has-error {
            border: 1px solid #a94442 !important;
            color: #db524b;
        }

    multiselect .dropdown-menu {
        max-height: 300px;
        min-width: 200px;
        overflow-y: auto;
    }

        multiselect .dropdown-menu .filter > input {
            width: 99%;
        }

        multiselect .dropdown-menu .filter .glyphicon {
            cursor: pointer;
            pointer-events: all;
        }

    multiselect .dropdown-menu {
        width: 100%;
        box-sizing: border-box;
        padding: 2px;
    }

    multiselect > .btn-group > button {
        padding-right: 20px;
    }

        multiselect > .btn-group > button > .caret {
            border-left: 4px solid transparent;
            border-right: 4px solid transparent;
            border-top: 4px solid black;
            right: 5px;
            top: 45%;
            position: absolute;
        }

    multiselect .dropdown-menu > li > a {
        padding: 3px 10px;
        cursor: pointer;
    }

        multiselect .dropdown-menu > li > a i {
            margin-right: 4px;
        }

.glyphicon-none:before {
    content: "\e013";
    color: transparent !important;
}
# Angular-UI Multiselect Checkbox Dropdown

This is an Angular-based dropdown that displays checkboxes for multiselection.

This a test to illustrate ng-change triggering after any selection.

### Links
  - [Blog](https://long2know.com/2015/07/angular-multiselect-dropdown/)
  - [Github](https://github.com/long2know/angular-directives-general)
(function() {
  var long2know;
  try {
    long2know = angular.module("long2know")
  } catch (err) {
    long2know = null;
  }

  if (!long2know) {
    angular.module('long2know.services', ['ngResource', 'ngAnimate']);
    angular.module('long2know.controllers', []);
    angular.module('long2know.directives', []);
    angular.module('long2know.constants', []);
    angular.module('long2know', [
      'long2know.services',
      'long2know.controllers',
      'long2know.directives',
      'long2know.constants'
    ]);
  }

  var multiselectParser = function($parse) {
    //                      00000111000000000000022200000000000000003333333333333330000000000044000
    var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;

    return {
      parse: function(input) {

        var match = input.match(TYPEAHEAD_REGEXP);
        if (!match) {
          throw new Error(
            'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
            ' but got "' + input + '".');
        }

        return {
          itemName: match[3],
          source: $parse(match[4]),
          viewMapper: $parse(match[2] || match[1]),
          modelMapper: $parse(match[1])
        };
      }
    };
  };

  var multiselect = function($parse, $timeout, $filter, $document, $compile, $window, $position, optionParser) {
    return {
      restrict: 'EA',
      require: ['ngModel', '?^form'],
      link: function(originalScope, element, attrs, ctrls) {
        var modelCtrl = ctrls[0];
        var formCtrl = (ctrls.length > 1 && typeof(ctrls[1]) !== 'undefined') ? ctrls[1] : null;

        //model setter executed upon match selection
        var $setModelValue = $parse(attrs.ngModel).assign;

        var
          modelValue = {},
          parserResult = optionParser.parse(attrs.options),
          isMultiple = attrs.multiple ? originalScope.$eval(attrs.multiple) : false,
          isAutoFocus = attrs.autoFocus ? originalScope.$eval(attrs.autoFocus) : false,
          isComplex = attrs.complexModels ? originalScope.$eval(attrs.complexModels) : false,
          enableFilter = attrs.enableFilter ? originalScope.$eval(attrs.enableFilter) : true,
          enableCheckAll = attrs.enableCheckAll ? originalScope.$eval(attrs.enableCheckAll) : true,
          enableUncheckAll = attrs.enableUncheckAll ? originalScope.$eval(attrs.enableUncheckAll) : true,
          header = attrs.header ? attrs.header : "Select",
          selectedHeader = attrs.selectedHeader ? attrs.selectedHeader : 'selected',
          selectLimit = attrs.selectLimit ? originalScope.$eval(attrs.selectLimit) : 0,
          useFiltered = attrs.selectLimitUseFiltered ? originalScope.$eval(attrs.selectLimitUseFiltered) : true,
          filterPlaceholder = attrs.filterPlaceholder ? attrs.filterPlaceholder : "Filter ..",
          checkAllLabel = attrs.checkAllLabel ? attrs.checkAllLabel : "Check all",
          uncheckAllLabel = attrs.uncheckAllLabel ? attrs.uncheckAllLabel : "Uncheck all",
          appendToBody = attrs.appendToBody ? originalScope.$eval(attrs.appendToBody) : false,
          required = false,
          lastSelectedLabel = '',
          scope = originalScope.$new(true),
          changeHandler = attrs.change || angular.noop,
          popUpEl = angular.element('<multiselect-popup></multiselect-popup>'),
          popupId = 'multiselect-' + scope.$id + '-' + Math.floor(Math.random() * 10000),
          timeoutEventPromise,
          eventDebounceTime = 200,

          isChecked = function(i) {
            return i.checked === true;
          },

          getFilteredItems = function() {
            var filteredItems = $filter("filter")(scope.items, scope.searchText);
            return filteredItems;
          },

          getFirstSelectedLabel = function() {
            for (var i = 0; i < scope.items.length; i++) {
              if (scope.items[i].checked) {
                return scope.items[i].label;
              }
            }
            return header;
          },
          canCheck = function() {
            var belowLimit = false;
            var atLimit = false;
            var aboveLimit = false;
            if (selectLimit === 0 || !isMultiple) {
              belowLimit = true;
              atLimit = false;
            } else {
              var checkedItems = scope.items.filter(isChecked);
              atLimit = checkedItems.length === selectLimit;
              aboveLimit = checkedItems.length > selectLimit;
              belowLimit = checkedItems.length < selectLimit;
            }
            scope.maxSelected = atLimit || aboveLimit;
            return atLimit || belowLimit;
          },
          getHeaderText = function() {
            var localHeader = header;
            var compareModel = modelValue;
            // var compareModel = modelCtrl.$modelValue 
            if (isEmpty(compareModel)) return scope.header = localHeader;
            if (isMultiple) {
              var isArray = compareModel instanceof Array;
              if (isArray && compareModel.length > 1) {
                localHeader = compareModel.length + ' ' + selectedHeader;
              } else {
                localHeader = getFirstSelectedLabel();
              }
            } else {
              localHeader = getFirstSelectedLabel();
            }
            scope.header = localHeader;
          },

          isEmpty = function(obj) {
            if (!obj) return true;
            if (!isComplex && obj) return false;
            if (obj.length && obj.length > 0) return false;
            for (var prop in obj)
              if (obj[prop]) return false;
            return true;
          },

          updateModel = function() {
            modelCtrl.$setViewValue(modelValue);
          },

          parseModel = function() {
            scope.items.length = 0;
            var model = parserResult.source(originalScope);
            if (!angular.isDefined(model)) return;
            var compareModel = modelValue;
            // var compareModel = modelCtrl.$modelValue 
            isArray = compareModel instanceof Array;
            for (var i = 0; i < model.length; i++) {
              var local = {};
              local[parserResult.itemName] = model[i];
              var value = parserResult.modelMapper(local)
              var isChecked = isArray ?
                (compareModel.indexOf(value.toString()) != -1 || compareModel.indexOf(value) != -1) :
                (!isEmpty(compareModel) && compareModel == value);
              var item = {
                label: parserResult.viewMapper(local),
                model: model[i],
                checked: isChecked
              };
              scope.items.push(item);
            }
            getHeaderText();
          },

          selectSingle = function(item) {
            if (item.checked) {
              scope.uncheckAll();
            } else {
              scope.uncheckAll();
              item.checked = true;
            }
            setModelValue(false);
            updateModel();
          },

          selectMultiple = function(item) {
            var isChanged = false;
            if (item.checked) {
              isChanged = true;
              item.checked = false;
              canCheck();
            } else if (!scope.maxSelected) {
              isChanged = true;
              item.checked = canCheck();
            }
            setModelValue(true);
            if (isChanged) {
              updateModel();
            }
          },

          getModelValue = function(item) {
            if (isComplex) {
              value = item.model;
            } else {
              var local = {};
              local[parserResult.itemName] = item.model;
              value = parserResult.modelMapper(local);
            }
            return value;
          },

          setModelValue = function(isMultiple) {
            var value;
            if (isMultiple) {
              value = [];
              angular.forEach(scope.items, function(item) {
                // If map simple values
                if (item.checked) {
                  if (isComplex) {
                    value.push(item.model);
                  } else {
                    var local = {};
                    local[parserResult.itemName] = item.model;
                    value.push(parserResult.modelMapper(local));
                  }
                }
              })
            } else {
              angular.forEach(scope.items, function(item) {
                if (item.checked) {
                  if (isComplex) {
                    value = item.model;
                    return false;
                  } else {
                    var local = {};
                    local[parserResult.itemName] = item.model;
                    value = parserResult.modelMapper(local);
                    return false;
                  }
                }
              })
            }
            scope.triggered = true;
            modelValue = value;
          },

          markChecked = function(newVal) {
            if (!angular.isArray(newVal)) {
              angular.forEach(scope.items, function(item) {
                var value = getModelValue(item);
                if (angular.equals(value, newVal)) {
                  item.checked = true;
                  return false;
                }
              });
            } else {
              var
                itemsToCheck = [],
                itemsToUncheck = [],
                itemValues = [],
                i = 0,
                j = 0;
              for (j = 0; j < scope.items.length; j++) {
                itemValues.push(getModelValue(scope.items[j]));
                itemsToUncheck.push(j);
              }

              for (i = 0; i < newVal.length; i++) {
                for (j = 0; j < itemValues.length; j++) {
                  if (angular.equals(itemValues[j], newVal[i])) {
                    itemsToCheck.push(scope.items[j]);
                    var index = itemsToUncheck.indexOf(j);
                    itemsToUncheck.splice(index, 1);
                    break;
                  }
                }
              }

              for (i = 0; i < itemsToCheck.length; i++) {
                itemsToCheck[i].checked = true;
              }

              for (i = 0; i < itemsToUncheck.length; i++) {
                scope.items[itemsToUncheck[i]].checked = false;
              }
            }
          },

          // recalculate actual position and set new values to scope
          // after digest loop is popup in right position
          recalculatePosition = function() {
            scope.position = appendToBody ? $position.offset($popup) : $position.position(element);
            scope.position.top += $popup.prop('offsetHeight');
          },

          fireRecalculating = function() {
            if (!scope.moveInProgress) {
              scope.moveInProgress = true;
              scope.$digest();
            }

            // Cancel previous timeout
            if (timeoutEventPromise) {
              $timeout.cancel(timeoutEventPromise);
            }

            // Debounced executing recalculate after events fired
            timeoutEventPromise = $timeout(function() {
              // if popup is visible
              if (scope.isOpen) {
                recalculatePosition();
              }
              scope.moveInProgress = false;
              scope.$digest();
            }, eventDebounceTime);
          };

        scope.items = [];
        scope.header = header;
        scope.multiple = isMultiple;
        scope.disabled = false;
        scope.filterPlaceholder = filterPlaceholder;
        scope.checkAllLabel = checkAllLabel;
        scope.uncheckAllLabel = uncheckAllLabel;
        scope.selectLimit = selectLimit;
        scope.enableFilter = enableFilter;
        scope.enableCheckAll = enableCheckAll;
        scope.enableUncheckAll = enableUncheckAll;
        scope.searchText = {
          label: ''
        };
        scope.isAutoFocus = isAutoFocus;
        scope.appendToBody = appendToBody;
        scope.moveInProgress = false;
        scope.popupId = popupId;
        scope.recalculatePosition = recalculatePosition;
        scope.isModelValueSet = false;

        originalScope.$on('$destroy', function() {
          scope.$destroy();
          $document.unbind('click', scope.clickHandler);
          if (appendToBody) {
            $('#' + popupId).remove();
          }
        });

        // bind events only if appendToBody params exist - performance feature
        if (appendToBody) {
          angular.element($window).bind('resize', fireRecalculating);
          $document.find('body').bind('scroll', fireRecalculating);
        }

        // required validator
        if (attrs.required || attrs.ngRequired) {
          required = true;
        }

        attrs.$observe('required', function(newVal) {
          required = newVal;
        });

        //watch disabled state
        scope.$watch(function() {
          return $parse(attrs.ngDisabled)(originalScope);
        }, function(newVal) {
          scope.disabled = newVal;
        });

        //watch single/multiple state for dynamically change single to multiple
        scope.$watch(function() {
          return $parse(attrs.multiple)(originalScope);
        }, function(newVal) {
          isMultiple = newVal || false;
        });

        //watch option changes for options that are populated dynamically
        scope.$watch(function() {
          return parserResult.source(originalScope);
        }, function(newVal) {
          if (angular.isDefined(newVal)) {
            parseModel();
            setModelValue(isMultiple);
          }
        }, true);

        ////watch model change  --> This has an issue in that it seems that all models are updated to the same value
        scope.$watch(function() {
          return modelCtrl.$modelValue;
        }, function(newVal, oldVal) {
          //when directive initializes, newVal is usually undefined. Also, if model value is already set in the controller
          //for preselected list then we need to mark checked in our scope item. But we don't want to do this every time the
          //model changes. We need to do this only if it is done outside directive scope, from controller, for example.
          if (!scope.triggered) {
            if (angular.isDefined(newVal)) {
              var isArray = newVal instanceof Array;
              if ((isArray && newVal.length == 0) || !isArray) {
                scope.uncheckAll();
              }
              markChecked(newVal);
              setModelValue(isMultiple);
              scope.isModelValueSet = true;
              // Technically, defining ngChange will already have a watcher triggering its handler
              // So, triggering it manually should be redundant
              //scope.$eval(changeHandler);
            } else if (scope.isModelValueSet) {
              // If the model value is cleared externally, and we previously had some things checked,
              // we need to uncheck them.
              scope.uncheckAll();
              scope.isModelValueSet = false;
            }
          }
          getHeaderText();
          canCheck();
          modelCtrl.$setValidity('required', scope.valid());
          scope.triggered = false;
        }, true);

        parseModel();
        var $popup = $compile(popUpEl)(scope);
        element.append($popup);
        $timeout(function() {
          recalculatePosition();
        }, 100);

        scope.valid = function validModel() {
          if (!required) return true;
          var value = modelCtrl.$modelValue;
          return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value != null);
        };

        scope.checkAll = function(setNgModel) {
          if (!isMultiple) return;
          var items = scope.items;
          var anyChecked = false;
          var totalChecked = 0;
          if (useFiltered) {
            items = getFilteredItems();
            angular.forEach(items, function(item) {
              item.checked = false;
            });
            totalChecked = scope.items.filter(isChecked).length;
          }
          if (selectLimit <= 0 || (items.length < selectLimit - totalChecked)) {
            angular.forEach(items, function(item) {
              item.checked = true;
            });
          } else {
            angular.forEach(items, function(item) {
              item.checked = false;
            });

            for (var i = 0; i < (selectLimit - totalChecked); i++) {
              items[i].checked = true;
            }
            scope.maxSelected = true;
          }
          setModelValue(true);
          
          if (setNgModel) {
            updateModel();
          }
        };

        scope.uncheckAll = function(setNgModel) {
          var items = useFiltered ? getFilteredItems() : scope.items;
          var anyChecked = false;
          angular.forEach(items, function(item) {
            if (item.checked) { anyChecked = true; }
            item.checked = false;
          });
          canCheck();
          if (isMultiple) {
            setModelValue(true);
          }
          
          if (anyChecked && setNgModel) {
            updateModel();
          }
        };

        scope.select = function(item) {
          if (isMultiple === false) {
            selectSingle(item);
            scope.toggleSelect();
          } else {
            selectMultiple(item);
          }
        };

        scope.clearFilter = function() {
          scope.searchText.label = '';
        };
      }
    };
  };

  var multiselectPopup = function($document) {
    return {
      restrict: 'E',
      replace: true,
      require: ['^ngModel', '?^form'],
      templateUrl: 'template/multiselect/multiselectPopup.html',
      link: function(scope, element, attrs, ctrls) {
        var $dropdown = element.find(".dropdown-menu");
        $dropdown.attr("id", scope.popupId);

        if (scope.appendToBody) {
          $document.find('body').append($dropdown);
        }

        var
          clickHandler = function(event) {
            if (elementMatchesAnyInArray(event.target, element.find(event.target.tagName)))
              return;

            if (scope.appendToBody) {
              if (elementMatchesAnyInArray(event.target, $dropdown.find(event.target.tagName)))
                return;
            }

            element.removeClass('open');
            scope.isOpen = false;
            $document.unbind('click', clickHandler);
            scope.$apply();
          },
          elementMatchesAnyInArray = function(element, elementArray) {
            for (var i = 0; i < elementArray.length; i++)
              if (element == elementArray[i])
                return true;
            return false;
          };

        scope.clickHandler = clickHandler;
        scope.isVisible = false;
        scope.isHeightChanged = true;

        var
          dropdownHeight,
          dropdownWidth;

        scope.toggleSelect = function() {
          if (element.hasClass('open') || scope.isOpen) {
            element.removeClass('open');
            scope.isOpen = false;
            $document.unbind('click', clickHandler);
          } else {
            element.addClass('open');
            scope.isOpen = true;
            $document.bind('click', clickHandler);
            if (scope.isAutoFocus) {
              scope.focus();
            }
            scope.recalculatePosition();
          }

          // Figure out if dropup
          var parent = element.parent();
          var windowScrollTop = $(window).scrollTop();
          var windowHeight = $(window).height();
          var windowWidth = $(window).width();
          var ulElement = element.find("ul:first");

          if (scope.isHeightChanged) {
            dropdownHeight = ulElement.height();
            dropdownWidth = ulElement.width();
            scope.isHeightChanged = false;
          }

          // If we have no height/width, the element isn't visisble - we can clone it and show it off screen to get
          // its visibile dimensions. Alternatively, we could just make the element visible and then adjust,
          // but this might result in some screen flicker... who knows?
          if (dropdownHeight <= 0 && dropdownWidth <= 0) {
            var clonedElement = $(ulElement)
              .clone()
              .css('position', 'fixed')
              .css('top', '0')
              .css('left', '-10000px')
              .appendTo(parent)
              .removeClass('ng-hide')
              .show();

            dropdownHeight = clonedElement.height();
            dropdownWidth = clonedElement.width();

            // Memory clean up - also, if you don't remove the clone from the DOM, IE11 increases the height of the HTML DOM element (buggy piece of junk!)
            clonedElement.remove();
            clonedElement = null;
          }

          // Determine if outside of visible range when dropping down
          var elementTop = element.offset().top + element.height() - windowScrollTop;
          var elementBottom = windowHeight - element.height() - element.offset().top + windowScrollTop;
          if ((elementBottom < dropdownHeight) && (elementTop > dropdownHeight)) {
            // Alert should drop up!
            scope.dropup = true;
          } else {
            scope.dropup = false;
          }

          // Figure out if we need left adjust
          if (element.offset().left + dropdownWidth >= windowWidth) {
            scope.isOffRight = true;
            var adjust = ((element.offset().left + dropdownWidth - windowWidth) + 10) * -1.0;
            ulElement.css("left", adjust.toString() + "px");
          } else {
            scope.isOffRight = false;
            ulElement.css("left", "0");
          }
        };

        scope.focus = function focus() {
          if (scope.enableFilter) {
            var searchBox = element.find('input')[0];
            searchBox.focus();
          }
        }
      }
    }
  };

  // IE11 doesn't enable the filter box when parent changes is using disabled attribute - so, use ng-disabled in your own HTML!
  angular.module("long2know").run(["$templateCache", function($templateCache) {
    $templateCache.put("template/multiselect/multiselectPopup.html",
      "<div class=\"btn-group\" ng-class=\"{ dropup: dropup, single: !multiple }\">" +
      "<button type=\"button\" class=\"btn btn-default dropdown-toggle\" ng-click=\"toggleSelect()\" ng-disabled=\"disabled\" ng-class=\"{'has-error': !valid()}\">" +
      "<span class=\"pull-left\" ng-bind=\"header\"></span>" +
      "<span class=\"caret pull-right\"></span>" +
      "</button>" +
      "<ul class=\"dropdown-menu multi-select-popup\" ng-show=\"isOpen && !moveInProgress\" ng-style=\"{ true: {top: position.top +'px', left: position.left +'px'}, false: {}}[appendToBody]\" style=\"display: block;\" role=\"listbox\" aria-hidden=\"{{!isOpen}}\">" +
      "<li ng-if=\"enableFilter\" class=\"filter-container\">" +
      "<div class=\"form-group has-feedback filter\">" +
      "<input class=\"form-control\" type=\"text\" ng-model=\"searchText.label\" placeholder=\"{{ filterPlaceholder }}\" />" +
      "<span class=\"glyphicon glyphicon-remove-circle form-control-feedback\" ng-click=\"clearFilter()\"></span>" +
      "</div>" +
      "</li>" +
      "<li ng-show=\"multiple && (enableCheckAll || enableUncheckAll)\">" +
      "<button ng-if=\"enableCheckAll\" type=\"button\" class=\"btn-link btn-small\" ng-click=\"checkAll(true)\"><i class=\"icon-ok\"></i> {{ checkAllLabel }}</button>" +
      "<button ng-if=\"enableUncheckAll\" type=\"button\" class=\"btn-link btn-small\" ng-click=\"uncheckAll(true)\"><i class=\"icon-remove\"></i> {{ uncheckAllLabel }}</button>" +
      "</li>" +
      "<li ng-show=\"maxSelected\">" +
      "<small>Selected maximum of </small><small ng-bind=\"selectLimit\"></small>" +
      "</li>" +
      "<li ng-repeat=\"i in items | filter:searchText\">" +
      //"<a ng-click=\"select(i); focus()\">" +
      "<a ng-click=\"select(i);\">" +
      "<i class=\"glyphicon\" ng-class=\"{'glyphicon-ok': i.checked, 'glyphicon-none': !i.checked}\"></i>" +
      "<span ng-bind=\"i.label\"></span>" +
      "</a>" +
      "</li>" +
      "</ul>" +
      "</div>");
  }]);

  multiselectParser.$inject = ['$parse'];
  multiselect.$inject = ['$parse', '$timeout', '$filter', '$document', '$compile', '$window', '$uibPosition', 'multiselectParser'];
  multiselectPopup.$inject = ['$document'];

  angular
    .module("long2know.services")
    .factory('multiselectParser', multiselectParser);

  angular
    .module('long2know.directives')
    .directive('multiselectPopup', multiselectPopup);

  angular
    .module('long2know.directives')
    .directive('multiselect', multiselect);
})()