3 var directiveModule = angular.module('angularjs-dropdown-multiselect', []);
5 directiveModule.directive('mfDropdownStaticInclude', ['$compile', function($compile) {
6 return function(scope, element, attrs) {
7 var template = attrs.mfDropdownStaticInclude;
8 var contents = element.html(template).contents();
9 $compile(contents)(scope);
13 directiveModule.directive('ngDropdownMultiselect', ['$filter', '$document', '$compile', '$parse', function($filter, $document, $compile, $parse) {
22 translationTexts: '=',
26 template: function(element, attrs) {
27 var checkboxes = attrs.checkboxes ? true : false;
28 var groups = attrs.groupBy ? true : false;
30 var template = '<div class="multiselect-parent btn-group dropdown-multiselect">';
31 template += '<button ng-disabled="disabled" type="button" class="dropdown-toggle" ng-class="settings.buttonClasses" ng-click="toggleDropdown()">{{getButtonText()}} <span class="caret"></span></button>';
32 template += '<ul class="dropdown-menu dropdown-menu-form" ng-if="open" ng-style="{display: open ? \'block\' : \'none\', height : settings.scrollable ? settings.scrollableHeight : \'auto\', overflow: \'auto\' }" >';
33 template += '<li ng-if="settings.showCheckAll && settings.selectionLimit !== 1"><a ng-keydown="keyDownLink($event)" data-ng-click="selectAll()" tabindex="-1" id="selectAll"><span class="glyphicon glyphicon-ok"></span> {{texts.checkAll}}</a>';
34 template += '<li ng-if="settings.showUncheckAll"><a ng-keydown="keyDownLink($event)" data-ng-click="deselectAll();" tabindex="-1" id="deselectAll"><span class="glyphicon glyphicon-remove"></span> {{texts.uncheckAll}}</a></li>';
35 template += '<li ng-if="settings.selectByGroups && ((settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll)" class="divider"></li>';
36 template += '<li ng-if="settings.selectByGroups && ((settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll)" class="divider"></li>';
37 template += '<li ng-repeat="currentGroup in settings.selectByGroups track by $index" ng-click="selectCurrentGroup(currentGroup)"><a ng-class="{\'dropdown-selected-group\': selectedGroup === currentGroup}" tabindex="-1">{{::texts.selectGroup}} {{::getGroupLabel(currentGroup)}}</a></li>';
38 template += '<li ng-if="settings.selectByGroups && settings.showEnableSearchButton" class="divider"></li>';
39 template += '<li ng-if="settings.showEnableSearchButton && settings.enableSearch"><a ng-keydown="keyDownLink($event); keyDownToggleSearch();" ng-click="toggleSearch($event);" tabindex="-1">{{texts.disableSearch}}</a></li>';
40 template += '<li ng-if="settings.showEnableSearchButton && !settings.enableSearch"><a ng-keydown="keyDownLink($event); keyDownToggleSearch();" ng-click="toggleSearch($event);" tabindex="-1">{{texts.enableSearch}}</a></li>';
41 template += '<li ng-if="(settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll || settings.showEnableSearchButton" class="divider"></li>';
42 template += '<li ng-if="settings.enableSearch"><div class="dropdown-header"><input type="text" class="form-control searchField" ng-keydown="keyDownSearchDefault($event); keyDownSearch($event, input.searchFilter);" ng-style="{width: \'100%\'}" ng-model="input.searchFilter" placeholder="{{texts.searchPlaceholder}}" /></li>';
43 template += '<li ng-if="settings.enableSearch" class="divider"></li>';
46 template += '<li ng-repeat-start="option in orderedItems | filter:getFilter(input.searchFilter)" ng-show="getPropertyForObject(option, settings.groupBy) !== getPropertyForObject(orderedItems[$index - 1], settings.groupBy)" role="presentation" class="dropdown-header">{{ getGroupLabel(getPropertyForObject(option, settings.groupBy)) }}</li>';
47 template += '<li ng-class="{\'active\': isChecked(getPropertyForObject(option,settings.idProp)) && settings.styleActive}" ng-repeat-end role="presentation">';
49 template += '<li ng-class="{\'active\': isChecked(getPropertyForObject(option,settings.idProp)) && settings.styleActive}" role="presentation" ng-repeat="option in options | filter:getFilter(input.searchFilter)">';
52 template += '<a ng-keydown="option.disabled || keyDownLink($event)" role="menuitem" class="option" tabindex="-1" ng-click="option.disabled || setSelectedItem(getPropertyForObject(option,settings.idProp), false, true)" ng-disabled="option.disabled">';
55 template += '<div class="checkbox"><label><input class="checkboxInput" type="checkbox" ng-click="checkboxClick($event, getPropertyForObject(option,settings.idProp))" ng-checked="isChecked(getPropertyForObject(option,settings.idProp))" /> <span mf-dropdown-static-include="{{settings.template}}"></div></label></span></a>';
57 template += '<span data-ng-class="{\'glyphicon glyphicon-ok\': isChecked(getPropertyForObject(option,settings.idProp))}"> </span> <span mf-dropdown-static-include="{{settings.template}}"></span></a>';
62 template += '<li class="divider" ng-show="settings.selectionLimit > 1"></li>';
63 template += '<li role="presentation" ng-show="settings.selectionLimit > 1"><a role="menuitem">{{selectedModel.length}} {{texts.selectionOf}} {{settings.selectionLimit}} {{texts.selectionCount}}</a></li>';
68 element.html(template);
70 link: function($scope, $element, $attrs) {
71 var $dropdownTrigger = $element.children()[0];
73 $scope.toggleDropdown = function() {
74 $scope.open = !$scope.open;
75 if ($scope.settings.keyboardControls) {
77 if ($scope.settings.selectionLimit === 1 && $scope.settings.enableSearch) {
78 setTimeout(function() {
79 angular.element($element)[0].querySelector('.searchField').focus();
82 setTimeout(function() {
83 angular.element($element)[0].querySelector('.option').focus();
90 $scope.checkboxClick = function($event, id) {
91 $scope.setSelectedItem(id, false, true);
92 $event.stopImmediatePropagation();
95 $scope.externalEvents = {
96 onItemSelect: angular.noop,
97 onItemDeselect: angular.noop,
98 onSelectAll: angular.noop,
99 onDeselectAll: angular.noop,
100 onInitDone: angular.noop,
101 onMaxSelectionReached: angular.noop,
102 onSelectionChanged: angular.noop
108 scrollableHeight: '300px',
110 displayProp: 'label',
112 externalIdProp: 'id',
116 showUncheckAll: true,
117 showEnableSearchButton: false,
118 closeOnSelect: false,
119 buttonClasses: 'btn btn-default',
120 closeOnDeselect: false,
121 groupBy: $attrs.groupBy || undefined,
122 groupByTextProvider: null,
123 smartButtonMaxItems: 0,
124 smartButtonTextConverter: angular.noop,
126 keyboardControls: false,
127 template: '{{getPropertyForObject(option, settings.displayProp)}}',
132 checkAll: 'Check All',
133 uncheckAll: 'Uncheck All',
134 selectionCount: 'checked',
136 searchPlaceholder: 'Search...',
137 buttonDefaultText: 'Select',
138 dynamicButtonTextSuffix: 'checked',
139 disableSearch: 'Disable search',
140 enableSearch: 'Enable search',
141 selectGroup: 'Select all:'
145 searchFilter: $scope.searchFilter || ''
148 if (angular.isDefined($scope.settings.groupBy)) {
149 $scope.$watch('options', function(newValue) {
150 if (angular.isDefined(newValue)) {
151 $scope.orderedItems = $filter('orderBy')(newValue, $scope.settings.groupBy);
156 $scope.$watch('selectedModel', function(newValue) {
157 if (!Array.isArray(newValue)) {
158 $scope.singleSelection = true;
160 $scope.singleSelection = false;
164 $scope.selectCurrentGroup = function(currentGroup) {
165 $scope.selectedModel.splice(0, $scope.selectedModel.length);
166 if ($scope.orderedItems) {
167 $scope.orderedItems.forEach(function(item) {
168 if (item[$scope.groupBy] === currentGroup) {
169 $scope.setSelectedItem($scope.getPropertyForObject(item, $scope.settings.idProp), false, false)
173 $scope.externalEvents.onSelectionChanged();
176 angular.extend($scope.settings, $scope.extraSettings || []);
177 angular.extend($scope.externalEvents, $scope.events || []);
178 angular.extend($scope.texts, $scope.translationTexts);
180 $scope.singleSelection = $scope.settings.selectionLimit === 1;
182 function getFindObj(id) {
185 if ($scope.settings.externalIdProp === '') {
186 findObj[$scope.settings.idProp] = id;
188 findObj[$scope.settings.externalIdProp] = id;
194 function clearObject(object) {
195 for (var prop in object) {
200 if ($scope.singleSelection) {
201 if (angular.isArray($scope.selectedModel) && $scope.selectedModel.length === 0) {
202 clearObject($scope.selectedModel);
206 if ($scope.settings.closeOnBlur) {
207 $document.on('click', function(e) {
209 var target = e.target.parentElement;
210 var parentFound = false;
212 while (angular.isDefined(target) && target !== null && !parentFound) {
213 if (!!target.className.split && contains(target.className.split(' '), 'multiselect-parent') && !parentFound) {
214 if (target === $dropdownTrigger) {
218 target = target.parentElement;
222 $scope.$apply(function() {
230 $scope.getGroupLabel = function(groupValue) {
231 if ($scope.settings.groupByTextProvider !== null) {
232 return $scope.settings.groupByTextProvider(groupValue);
238 $scope.getButtonText = function() {
239 if ($scope.settings.dynamicTitle && ($scope.selectedModel.length > 0 || (angular.isObject($scope.selectedModel) && Object.keys($scope.selectedModel).length > 0))) {
240 if ($scope.settings.smartButtonMaxItems > 0) {
243 angular.forEach($scope.options, function(optionItem) {
244 if ($scope.isChecked($scope.getPropertyForObject(optionItem, $scope.settings.idProp))) {
245 var displayText = $scope.getPropertyForObject(optionItem, $scope.settings.displayProp);
246 var converterResponse = $scope.settings.smartButtonTextConverter(displayText, optionItem);
248 itemsText.push(converterResponse ? converterResponse : displayText);
252 if ($scope.selectedModel.length > $scope.settings.smartButtonMaxItems) {
253 itemsText = itemsText.slice(0, $scope.settings.smartButtonMaxItems);
254 itemsText.push('...');
257 return itemsText.join(', ');
261 if ($scope.singleSelection) {
262 totalSelected = ($scope.selectedModel !== null && angular.isDefined($scope.selectedModel[$scope.settings.idProp])) ? 1 : 0;
264 totalSelected = angular.isDefined($scope.selectedModel) ? $scope.selectedModel.length : 0;
267 if (totalSelected === 0) {
268 return $scope.texts.buttonDefaultText;
270 return totalSelected + ' ' + $scope.texts.dynamicButtonTextSuffix;
274 return $scope.texts.buttonDefaultText;
278 $scope.getPropertyForObject = function(object, property) {
279 if (angular.isDefined(object) && object.hasOwnProperty(property)) {
280 return object[property];
286 $scope.selectAll = function() {
288 $scope.deselectAll(true);
289 $scope.externalEvents.onSelectAll();
291 searchResult = $filter('filter')($scope.options, $scope.getFilter($scope.input.searchFilter));
292 angular.forEach(searchResult, function(value) {
293 $scope.setSelectedItem(value[$scope.settings.idProp], true, false);
295 $scope.externalEvents.onSelectionChanged();
296 $scope.selectedGroup = null;
299 $scope.deselectAll = function(dontSendEvent) {
300 dontSendEvent = dontSendEvent || false;
302 if (!dontSendEvent) {
303 $scope.externalEvents.onDeselectAll();
306 if ($scope.singleSelection) {
307 clearObject($scope.selectedModel);
309 $scope.selectedModel.splice(0, $scope.selectedModel.length);
311 if (!dontSendEvent) {
312 $scope.externalEvents.onSelectionChanged();
314 $scope.selectedGroup = null;
317 $scope.setSelectedItem = function(id, dontRemove, fireSelectionChange) {
318 var findObj = getFindObj(id);
321 if ($scope.settings.externalIdProp === '') {
322 finalObj = find($scope.options, findObj);
327 if ($scope.singleSelection) {
328 clearObject($scope.selectedModel);
329 angular.extend($scope.selectedModel, finalObj);
330 $scope.externalEvents.onItemSelect(finalObj);
331 if ($scope.settings.closeOnSelect || $scope.settings.closeOnDeselect) $scope.open = false;
333 dontRemove = dontRemove || false;
335 var exists = findIndex($scope.selectedModel, findObj) !== -1;
337 if (!dontRemove && exists) {
338 $scope.selectedModel.splice(findIndex($scope.selectedModel, findObj), 1);
339 $scope.externalEvents.onItemDeselect(findObj);
340 if ($scope.settings.closeOnDeselect) $scope.open = false;
341 } else if (!exists && ($scope.settings.selectionLimit === 0 || $scope.selectedModel.length < $scope.settings.selectionLimit)) {
342 $scope.selectedModel.push(finalObj);
343 $scope.externalEvents.onItemSelect(finalObj);
344 if ($scope.settings.closeOnSelect) $scope.open = false;
345 if ($scope.settings.selectionLimit > 0 && $scope.selectedModel.length === $scope.settings.selectionLimit) {
346 $scope.externalEvents.onMaxSelectionReached();
350 if (fireSelectionChange) {
351 $scope.externalEvents.onSelectionChanged();
353 $scope.selectedGroup = null;
356 $scope.isChecked = function(id) {
357 if ($scope.singleSelection) {
358 return $scope.selectedModel !== null && angular.isDefined($scope.selectedModel[$scope.settings.idProp]) && $scope.selectedModel[$scope.settings.idProp] === getFindObj(id)[$scope.settings.idProp];
361 return findIndex($scope.selectedModel, getFindObj(id)) !== -1;
364 $scope.externalEvents.onInitDone();
366 $scope.keyDownLink = function(event) {
367 var sourceScope = angular.element(event.target).scope();
369 var parent = event.target.parentNode;
370 if (!$scope.settings.keyboardControls) {
373 if (event.keyCode === 13 || event.keyCode === 32) { // enter
374 event.preventDefault();
375 if (!!sourceScope.option) {
376 $scope.setSelectedItem($scope.getPropertyForObject(sourceScope.option, $scope.settings.idProp), false, true);
377 } else if (event.target.id === 'deselectAll') {
378 $scope.deselectAll();
379 } else if (event.target.id === 'selectAll') {
382 } else if (event.keyCode === 38) { // up arrow
383 event.preventDefault();
384 if (!!parent.previousElementSibling) {
385 nextOption = parent.previousElementSibling.querySelector('a') || parent.previousElementSibling.querySelector('input');
387 while (!nextOption && !!parent) {
388 parent = parent.previousElementSibling;
390 nextOption = parent.querySelector('a') || parent.querySelector('input');
396 } else if (event.keyCode === 40) { // down arrow
397 event.preventDefault();
398 if (!!parent.nextElementSibling) {
399 nextOption = parent.nextElementSibling.querySelector('a') || parent.nextElementSibling.querySelector('input');
401 while (!nextOption && !!parent) {
402 parent = parent.nextElementSibling;
404 nextOption = parent.querySelector('a') || parent.querySelector('input');
410 } else if (event.keyCode === 27) {
411 event.preventDefault();
413 $scope.toggleDropdown();
417 $scope.keyDownSearchDefault = function(event) {
418 var parent = event.target.parentNode.parentNode;
420 if (!$scope.settings.keyboardControls) {
423 if (event.keyCode === 9 || event.keyCode === 40) { //tab
424 event.preventDefault();
425 setTimeout(function() {
426 angular.element($element)[0].querySelector('.option').focus();
428 } else if (event.keyCode === 38) {
429 event.preventDefault();
430 if (!!parent.previousElementSibling) {
431 nextOption = parent.previousElementSibling.querySelector('a') || parent.previousElementSibling.querySelector('input');
433 while (!nextOption && !!parent) {
434 parent = parent.previousElementSibling;
436 nextOption = parent.querySelector('a') || parent.querySelector('input');
442 } else if (event.keyCode === 27) {
443 event.preventDefault();
445 $scope.toggleDropdown();
449 $scope.keyDownSearch = function(event, searchFilter) {
451 if (!$scope.settings.keyboardControls) {
454 if (event.keyCode === 13) {
455 if ($scope.settings.selectionLimit === 1 && $scope.settings.enableSearch) {
456 searchResult = $filter('filter')($scope.options, $scope.getFilter(searchFilter));
457 if (searchResult.length === 1) {
458 $scope.setSelectedItem($scope.getPropertyForObject(searchResult[0], $scope.settings.idProp), false, true);
460 } else if ($scope.settings.enableSearch) {
466 $scope.getFilter = function(searchFilter) {
468 filter[$scope.settings.searchField] = searchFilter;
472 $scope.toggleSearch = function($event) {
474 $event.stopPropagation();
476 $scope.settings.enableSearch = !$scope.settings.enableSearch;
477 if (!$scope.settings.enableSearch) {
478 $scope.input.searchFilter = '';
482 $scope.keyDownToggleSearch = function() {
483 if (!$scope.settings.keyboardControls) {
486 if (event.keyCode === 13) {
487 $scope.toggleSearch();
488 if ($scope.settings.enableSearch) {
491 angular.element($element)[0].querySelector('.searchField').focus();
495 angular.element($element)[0].querySelector('.option').focus();
503 function contains(collection, target) {
504 var containsTarget = false;
505 collection.some(function(object) {
506 if (object === target) {
507 containsTarget = true;
511 return containsTarget;
514 function find(collection, properties) {
517 collection.some(function(object) {
518 var hasAllSameProperties = true;
519 Object.keys(properties).forEach(function(key) {
520 if (object[key] !== properties[key]) {
521 hasAllSameProperties = false;
524 if (hasAllSameProperties) {
533 function findIndex(collection, properties) {
537 collection.some(function(object) {
538 var hasAllSameProperties = true;
540 Object.keys(properties).forEach(function(key) {
541 if (object[key] !== properties[key]) {
542 hasAllSameProperties = false;
545 if (hasAllSameProperties) {