diff --git a/.gitignore b/.gitignore index e550e4f..2dc3a30 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ Thumbs.db # Node Modules Directory # ########################## /node_modules/* +/bower_components/* diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..305cad9 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + $PROJECT_DIR$/gulpfile.js + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1451827150290 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dist/app.css b/dist/app.css new file mode 100644 index 0000000..0554d84 --- /dev/null +++ b/dist/app.css @@ -0,0 +1,10 @@ +.fixed-width{ + width: 150px; +} + +am-multiselect .dropdown-menu > li > .word-wrapped +{ + word-wrap: break-word; !important; + word-break: break-all; !important; + white-space: normal; !important; +} \ No newline at end of file diff --git a/dist/app.js b/dist/app.js index 7f14db4..1c687f6 100644 --- a/dist/app.js +++ b/dist/app.js @@ -4,14 +4,19 @@ angular.module('app', ['am.multiselect']) $scope.cars = [ {id:1, name: 'Audi'}, {id:2, name: 'BMW'}, - {id:3, name: 'Honda'} + {id:3, name: 'Honda'}, + {id:4, name: 'A very very long name with spaces. It never ends at all. Keeps going on and on and on. So what can you do.'}, + {id:5, name: 'AndThereComesTheNoSpaceMonsterThatYouHateToSeeInYourWebPageWhatCanYouDoAboutThis'} ]; $scope.selectedCar = []; $scope.fruits = [ {id: 1, name: 'Apple'}, {id: 2, name: 'Orange'}, - {id: 3, name: 'Banana'} + {id: 3, name: 'Banana: with some long story like there usual travel across the atlantic from south worm world to cold dizzy northern world.'} ]; $scope.selectedFruit = null; + $scope.OnSelectionChange = function(event){ + console.log(event); + } }]); diff --git a/dist/index.html b/dist/index.html index b32f42e..a36c6d7 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,28 +1,64 @@ Angular multiselect - + - + + - + -

Example

- -
- {{selectedCar}} -
- Single select: - -
- {{selectedFruit}} -
+

Example:Multiple selection with whole object selected items

+

+ Long List item ( with or without word spacing) are wrapped into multi line. + Width are fixed to 150px. +

+<am-multiselect class="input-lg" multiple="true" ms-selected="There are {{selectedCar.length}} car(s) selected"
+                ng-model="selectedCar" ms-header="Select Some Cars"
+                options="c as c.name for c in cars" ms-id="id" list-css="fixed-width" list-item-css="word-wrapped"
+                change="selected()"  onchange="OnSelectionChange(event)"> </am-multiselect>
+
+
+    With CSS:
+    .fixed-width
+    {
+        width: 150px;
+    }
+
+    am-multiselect .dropdown-menu > li > .word-wrapped
+    {
+        word-wrap: break-word; !important;
+        word-break: break-all; !important;
+        white-space: normal; !important;
+    }
+
+
+
+ +

+ +
+ {{selectedCar}} +
+

Single select: with only display attribute as selected item.

+

+ + <am-multiselect class="input-lg" + ng-model="selectedFruit" + options="c.name for c in fruits" + change="selected()"> </am-multiselect> + +

+ +
+ {{selectedFruit}} +
diff --git a/dist/multiselect-tpls.js b/dist/multiselect-tpls.js index 750bfe8..c212829 100644 --- a/dist/multiselect-tpls.js +++ b/dist/multiselect-tpls.js @@ -2,322 +2,353 @@ angular.module('am.multiselect', []) // from bootstrap-ui typeahead parser -.factory('optionParser', ['$parse', 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 + '".'); + .factory('optionParser', ['$parse', 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]) + }; } - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; - } - }; -}]) - -.directive('amMultiselect', ['$parse', '$document', '$compile', '$interpolate', 'optionParser', - - function ($parse, $document, $compile, $interpolate, optionParser) { - return { - restrict: 'E', - require: 'ngModel', - link: function (originalScope, element, attrs, modelCtrl) { - // Redefine isEmpty - this allows this to work on at least Angular 1.2.x - var isEmpty = modelCtrl.$isEmpty; - modelCtrl.$isEmpty = function(value) { - return isEmpty(value) || (angular.isArray(value) && value.length == 0); }; + }]) - var exp = attrs.options, - parsedResult = optionParser.parse(exp), - isMultiple = attrs.multiple ? true : false, - required = false, - scope = originalScope.$new(), - changeHandler = attrs.change || angular.noop; - - scope.items = []; - scope.header = 'Select'; - scope.multiple = isMultiple; - scope.disabled = false; - scope.onBlur = attrs.ngBlur || angular.noop; - - originalScope.$on('$destroy', function () { - scope.$destroy(); - }); - - var popUpEl = angular.element(''); - - // 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.disabled)(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 parsedResult.source(originalScope); - }, function (newVal) { - if (angular.isDefined(newVal)) - parseModel(); - }, true); - - // watch model change - scope.$watch(function () { - return modelCtrl.$modelValue; - }, function (newVal, oldVal) { - // when directive initialize, newVal usually undefined. Also, if model value 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 - // model changes. We need to do this only if it is done outside directive scope, from controller, for example. - if (angular.isDefined(newVal)) { - markChecked(newVal); - scope.$eval(changeHandler); - } - getHeaderText(); - modelCtrl.$setValidity('required', scope.valid()); - }, true); - - function parseModel() { - scope.items.length = 0; - var model = parsedResult.source(originalScope); - if(!angular.isDefined(model)) return; - for (var i = 0; i < model.length; i++) { - var local = {}; - local[parsedResult.itemName] = model[i]; - scope.items.push({ - label: parsedResult.viewMapper(local), - model: parsedResult.modelMapper(local), - checked: false - }); - } - } + .directive('amMultiselect', ['$parse', '$document', '$compile', '$interpolate', 'optionParser', + + function ($parse, $document, $compile, $interpolate, optionParser) { + return { + restrict: 'E', + require: 'ngModel', + link: function (originalScope, element, attrs, modelCtrl) { + // Redefine isEmpty - this allows this to work on at least Angular 1.2.x + var isEmpty = modelCtrl.$isEmpty; + modelCtrl.$isEmpty = function (value) { + return isEmpty(value) || (angular.isArray(value) && value.length == 0); + }; + + var exp = attrs.options, + parsedResult = optionParser.parse(exp), + isMultiple = attrs.multiple ? true : false, + required = false, + scope = originalScope.$new(), + changeHandler = attrs.change || angular.noop, + onChange = attrs.onchange || angular.noop; + + scope.items = []; + scope.header = 'Select'; + scope.multiple = isMultiple; + scope.disabled = false; + scope.onBlur = attrs.ngBlur || angular.noop; + scope.id = attrs.msId ? attrs.msId : null; + scope.listCss = attrs.listCss ? attrs.listCss : null; + scope.listItemCss = attrs.listItemCss ? attrs.listItemCss : null; + + scope.getListCss = function () { + var css = 'dropdown-menu'; + if (scope.listCss) + css = css + ' ' + scope.listCss; + return css; + }; + + scope.getListItemCss = function () { + return scope.listItemCss ? scope.listItemCss : ''; + }; + + + originalScope.$on('$destroy', function () { + scope.$destroy(); + }); - parseModel(); + var popUpEl = angular.element(''); - element.append($compile(popUpEl)(scope)); + // required validator + if (attrs.required || attrs.ngRequired) { + required = true; + } + attrs.$observe('required', function (newVal) { + required = newVal; + }); - function getHeaderText() { - if (is_empty(modelCtrl.$modelValue)) return scope.header = attrs.msHeader || 'Select'; + // watch disabled state + scope.$watch(function () { + return $parse(attrs.disabled)(originalScope); + }, function (newVal) { + scope.disabled = newVal; + }); - if (isMultiple) { - if (attrs.msSelected) { - scope.header = $interpolate(attrs.msSelected)(scope); - } else { - if (modelCtrl.$modelValue.length == 1) { - for(var i = 0; i < scope.items.length; i++) { - if(scope.items[i].model === modelCtrl.$modelValue[0]) { - scope.header = scope.items[i].label; - } + // 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 parsedResult.source(originalScope); + }, function (newVal) { + if (angular.isDefined(newVal)) + parseModel(); + }, true); + + // watch model change + scope.$watch(function () { + return modelCtrl.$modelValue; + }, function (newVal, oldVal) { + // when directive initialize, newVal usually undefined. Also, if model value 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 + // model changes. We need to do this only if it is done outside directive scope, from controller, for example. + if (angular.isDefined(newVal)) { + markChecked(newVal); + scope.$eval(changeHandler); + } + getHeaderText(); + modelCtrl.$setValidity('required', scope.valid()); + }, true); + + function parseModel() { + scope.items.length = 0; + var model = parsedResult.source(originalScope); + if (!angular.isDefined(model)) return; + for (var i = 0; i < model.length; i++) { + var local = {}; + local[parsedResult.itemName] = model[i]; + var item = { + label: parsedResult.viewMapper(local), + model: parsedResult.modelMapper(local), + checked: false + }; + if (scope.id) + item.id = item.model[scope.id]; + + scope.items.push(item); } - } else { - scope.header = modelCtrl.$modelValue.length + ' ' + 'selected'; } - } - } else { - if(angular.isString(modelCtrl.$modelValue)){ - scope.header = modelCtrl.$modelValue; - } else { - var local = {}; - local[parsedResult.itemName] = modelCtrl.$modelValue; - scope.header = parsedResult.viewMapper(local) || scope.items[modelCtrl.$modelValue].label; - } - } - } - function is_empty(obj) { - if (angular.isNumber(obj)) return false; - if (obj && obj.length && obj.length > 0) return false; - for (var prop in obj) if (obj[prop]) return false; - return true; - }; + parseModel(); + + element.append($compile(popUpEl)(scope)); + + function getHeaderText() { + if (is_empty(modelCtrl.$modelValue)) return scope.header = attrs.msHeader || 'Select'; + + if (isMultiple) { + if (attrs.msSelected) { + scope.header = $interpolate(attrs.msSelected)(scope); + } else { + if (modelCtrl.$modelValue.length == 1) { + for (var i = 0; i < scope.items.length; i++) { + if (scope.items[i].model === modelCtrl.$modelValue[0]) { + scope.header = scope.items[i].label; + } + } + } else { + scope.header = modelCtrl.$modelValue.length + ' ' + 'selected'; + } + } + } else { + if (angular.isString(modelCtrl.$modelValue)) { + scope.header = modelCtrl.$modelValue; + } else { + var local = {}; + local[parsedResult.itemName] = modelCtrl.$modelValue; + scope.header = parsedResult.viewMapper(local) || scope.items[modelCtrl.$modelValue].label; + } + } + } - scope.valid = function validModel() { - if(!required) return true; - var value = modelCtrl.$modelValue; - return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value != null); - }; + function is_empty(obj) { + if (angular.isNumber(obj)) return false; + if (obj && obj.length && obj.length > 0) return false; + for (var prop in obj) if (obj[prop]) return false; + return true; + }; + + scope.valid = function validModel() { + if (!required) return true; + var value = modelCtrl.$modelValue; + return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value != null); + }; + + function selectSingle(item) { + if (item.checked) { + scope.uncheckAll(); + } else { + scope.uncheckAll(); + item.checked = !item.checked; + } + setModelValue(false); + invokeOnChange(item); + } - function selectSingle(item) { - if (item.checked) { - scope.uncheckAll(); - } else { - scope.uncheckAll(); - item.checked = !item.checked; - } - setModelValue(false); - } + function invokeOnChange(item) { + scope.$eval(onChange, { + event: { + data: item.model, + action: item.checked ? 'selected' : 'unselected' + } + }); + } - function selectMultiple(item) { - item.checked = !item.checked; - setModelValue(true); - } + function selectMultiple(item) { + item.checked = !item.checked; + setModelValue(true); + invokeOnChange(item); + } - function setModelValue(isMultiple) { - var value = null; - - if (isMultiple) { - value = []; - angular.forEach(scope.items, function (item) { - if (item.checked) value.push(item.model); - }) - } else { - angular.forEach(scope.items, function (item) { - if (item.checked) { - value = item.model; - return false; + function setModelValue(isMultiple) { + var value = null; + + if (isMultiple) { + value = []; + angular.forEach(scope.items, function (item) { + if (item.checked) value.push(item.model); + }) + } else { + angular.forEach(scope.items, function (item) { + if (item.checked) { + value = item.model; + return false; + } + }) + } + modelCtrl.$setViewValue(value); } - }) - } - modelCtrl.$setViewValue(value); - } - function markChecked(newVal) { - if (!angular.isArray(newVal)) { - angular.forEach(scope.items, function (item) { - if (angular.equals(item.model, newVal)) { - scope.uncheckAll(); - item.checked = true; - setModelValue(false); - return false; + function markChecked(newVal) { + if (!angular.isArray(newVal)) { + angular.forEach(scope.items, function (item) { + if (angular.equals(item.model, newVal)) { + scope.uncheckAll(); + item.checked = true; + setModelValue(false); + return false; + } + }); + } else { + angular.forEach(scope.items, function (item) { + item.checked = false; + angular.forEach(newVal, function (i) { + if (angular.equals(item.model, i)) { + item.checked = true; + } + }); + }); + } } - }); - } else { - angular.forEach(scope.items, function (item) { - item.checked = false; - angular.forEach(newVal, function (i) { - if (angular.equals(item.model, i)) { + + scope.checkAll = function () { + if (!isMultiple) return; + angular.forEach(scope.items, function (item) { item.checked = true; + }); + setModelValue(true); + }; + + scope.uncheckAll = function () { + angular.forEach(scope.items, function (item) { + item.checked = false; + }); + setModelValue(true); + }; + + scope.select = function (item) { + if (isMultiple === false) { + selectSingle(item); + scope.toggleSelect(); + } else { + selectMultiple(item); } - }); - }); - } - } - - scope.checkAll = function () { - if (!isMultiple) return; - angular.forEach(scope.items, function (item) { - item.checked = true; - }); - setModelValue(true); - }; - - scope.uncheckAll = function () { - angular.forEach(scope.items, function (item) { - item.checked = false; - }); - setModelValue(true); - }; - - scope.select = function (item) { - if (isMultiple === false) { - selectSingle(item); - scope.toggleSelect(); - } else { - selectMultiple(item); - } - } - } - }; -}]) - -.directive('amMultiselectPopup', ['$document', '$filter', function ($document, $filter) { - return { - restrict: 'E', - scope: false, - replace: true, - templateUrl: function (element, attr) { - return attr.templateUrl || 'multiselect.tmpl.html'; - }, - link: function (scope, element, attrs) { - - scope.selectedIndex = null; - scope.isVisible = false; - - scope.toggleSelect = function () { - if (element.hasClass('open')) { - element.removeClass('open'); - $document.unbind('click', clickHandler); - scope.$parent.$eval(scope.onBlur); - } else { - element.addClass('open'); - $document.bind('click', clickHandler); - scope.focus(); + } } }; + }]) + + .directive('amMultiselectPopup', ['$document', '$filter', function ($document, $filter) { + return { + restrict: 'E', + scope: false, + replace: true, + templateUrl: function (element, attr) { + return attr.templateUrl || 'multiselect.tmpl.html'; + }, + link: function (scope, element, attrs) { + + scope.selectedIndex = null; + scope.isVisible = false; + + scope.toggleSelect = function () { + if (element.hasClass('open')) { + element.removeClass('open'); + $document.unbind('click', clickHandler); + scope.$parent.$eval(scope.onBlur); + } else { + element.addClass('open'); + $document.bind('click', clickHandler); + scope.focus(); + } + }; - function clickHandler(event) { - if (elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) { - scope.$parent.$eval(scope.onBlur); - } else { - element.removeClass('open'); - $document.unbind('click', clickHandler); - scope.$apply(); + function clickHandler(event) { + if (elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) { + scope.$parent.$eval(scope.onBlur); + } else { + element.removeClass('open'); + $document.unbind('click', clickHandler); + scope.$apply(); + } } - } - scope.focus = function focus(){ - var searchBox = element.find('input')[0]; - if (searchBox) { - searchBox.focus(); + scope.focus = function focus() { + var searchBox = element.find('input')[0]; + if (searchBox) { + searchBox.focus(); + } } - } - scope.keydown = function (event) { - var list = $filter('filter')(scope.items, scope.searchText), - keyCode = (event.keyCode || event.which); + scope.keydown = function (event) { + var list = $filter('filter')(scope.items, scope.searchText), + keyCode = (event.keyCode || event.which); - if(keyCode === 13){ // On enter - if(list[scope.selectedIndex]){ - scope.select(list[scope.selectedIndex]); // (un)select item + if (keyCode === 13) { // On enter + if (list[scope.selectedIndex]) { + scope.select(list[scope.selectedIndex]); // (un)select item + } + } else if (keyCode === 38) { // On arrow up + scope.selectedIndex = scope.selectedIndex === null ? list.length - 1 : scope.selectedIndex - 1; + } else if (keyCode === 40) { // On arrow down + scope.selectedIndex = scope.selectedIndex === null ? 0 : scope.selectedIndex + 1; + } else { // On any other key + scope.selectedIndex = null; } - }else if(keyCode === 38){ // On arrow up - scope.selectedIndex = scope.selectedIndex===null ? list.length-1 : scope.selectedIndex-1; - }else if(keyCode === 40){ // On arrow down - scope.selectedIndex = scope.selectedIndex===null ? 0 : scope.selectedIndex+1; - }else{ // On any other key - scope.selectedIndex = null; - } - if(scope.selectedIndex < 0){ // Select last in list - scope.selectedIndex = list.length-1; - }else if(scope.selectedIndex > list.length-1){ // Set selection to first item in list - scope.selectedIndex = 0; - } - }; + if (scope.selectedIndex < 0) { // Select last in list + scope.selectedIndex = list.length - 1; + } else if (scope.selectedIndex > list.length - 1) { // Set selection to first item in list + scope.selectedIndex = 0; + } + }; - var elementMatchesAnyInArray = function (element, elementArray) { - for (var i = 0; i < elementArray.length; i++) - if (element == elementArray[i]) - return true; - return false; + var elementMatchesAnyInArray = function (element, elementArray) { + for (var i = 0; i < elementArray.length; i++) + if (element == elementArray[i]) + return true; + return false; + } } } - } -}]); + }]); -angular.module("am.multiselect").run(["$templateCache", function($templateCache) {$templateCache.put("multiselect.tmpl.html","
\r\n \r\n
    \r\n
  • \r\n \r\n
  • \r\n
  • \r\n \r\n \r\n
  • \r\n
  • \r\n \r\n {{i.label}}\r\n
  • \r\n
\r\n
\r\n");}]); \ No newline at end of file +angular.module("am.multiselect").run(["$templateCache", function($templateCache) {$templateCache.put("multiselect.tmpl.html","
\n \n \n \n
\n");}]); \ No newline at end of file diff --git a/dist/multiselect.css b/dist/multiselect.css index 3b817ad..0cee690 100644 --- a/dist/multiselect.css +++ b/dist/multiselect.css @@ -6,6 +6,7 @@ am-multiselect .dropdown-menu{ am-multiselect .dropdown-menu > li > a { padding: 3px 10px; cursor:pointer; + } am-multiselect .dropdown-menu > li.selected { diff --git a/dist/multiselect.js b/dist/multiselect.js index d107725..00de8a5 100644 --- a/dist/multiselect.js +++ b/dist/multiselect.js @@ -2,320 +2,351 @@ angular.module('am.multiselect', []) // from bootstrap-ui typeahead parser -.factory('optionParser', ['$parse', 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 + '".'); + .factory('optionParser', ['$parse', 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]) + }; } - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; - } - }; -}]) - -.directive('amMultiselect', ['$parse', '$document', '$compile', '$interpolate', 'optionParser', - - function ($parse, $document, $compile, $interpolate, optionParser) { - return { - restrict: 'E', - require: 'ngModel', - link: function (originalScope, element, attrs, modelCtrl) { - // Redefine isEmpty - this allows this to work on at least Angular 1.2.x - var isEmpty = modelCtrl.$isEmpty; - modelCtrl.$isEmpty = function(value) { - return isEmpty(value) || (angular.isArray(value) && value.length == 0); }; + }]) - var exp = attrs.options, - parsedResult = optionParser.parse(exp), - isMultiple = attrs.multiple ? true : false, - required = false, - scope = originalScope.$new(), - changeHandler = attrs.change || angular.noop; - - scope.items = []; - scope.header = 'Select'; - scope.multiple = isMultiple; - scope.disabled = false; - scope.onBlur = attrs.ngBlur || angular.noop; - - originalScope.$on('$destroy', function () { - scope.$destroy(); - }); - - var popUpEl = angular.element(''); - - // 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.disabled)(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 parsedResult.source(originalScope); - }, function (newVal) { - if (angular.isDefined(newVal)) - parseModel(); - }, true); - - // watch model change - scope.$watch(function () { - return modelCtrl.$modelValue; - }, function (newVal, oldVal) { - // when directive initialize, newVal usually undefined. Also, if model value 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 - // model changes. We need to do this only if it is done outside directive scope, from controller, for example. - if (angular.isDefined(newVal)) { - markChecked(newVal); - scope.$eval(changeHandler); - } - getHeaderText(); - modelCtrl.$setValidity('required', scope.valid()); - }, true); - - function parseModel() { - scope.items.length = 0; - var model = parsedResult.source(originalScope); - if(!angular.isDefined(model)) return; - for (var i = 0; i < model.length; i++) { - var local = {}; - local[parsedResult.itemName] = model[i]; - scope.items.push({ - label: parsedResult.viewMapper(local), - model: parsedResult.modelMapper(local), - checked: false - }); - } - } + .directive('amMultiselect', ['$parse', '$document', '$compile', '$interpolate', 'optionParser', + + function ($parse, $document, $compile, $interpolate, optionParser) { + return { + restrict: 'E', + require: 'ngModel', + link: function (originalScope, element, attrs, modelCtrl) { + // Redefine isEmpty - this allows this to work on at least Angular 1.2.x + var isEmpty = modelCtrl.$isEmpty; + modelCtrl.$isEmpty = function (value) { + return isEmpty(value) || (angular.isArray(value) && value.length == 0); + }; + + var exp = attrs.options, + parsedResult = optionParser.parse(exp), + isMultiple = attrs.multiple ? true : false, + required = false, + scope = originalScope.$new(), + changeHandler = attrs.change || angular.noop, + onChange = attrs.onchange || angular.noop; + + scope.items = []; + scope.header = 'Select'; + scope.multiple = isMultiple; + scope.disabled = false; + scope.onBlur = attrs.ngBlur || angular.noop; + scope.id = attrs.msId ? attrs.msId : null; + scope.listCss = attrs.listCss ? attrs.listCss : null; + scope.listItemCss = attrs.listItemCss ? attrs.listItemCss : null; + + scope.getListCss = function () { + var css = 'dropdown-menu'; + if (scope.listCss) + css = css + ' ' + scope.listCss; + return css; + }; + + scope.getListItemCss = function () { + return scope.listItemCss ? scope.listItemCss : ''; + }; + + + originalScope.$on('$destroy', function () { + scope.$destroy(); + }); - parseModel(); + var popUpEl = angular.element(''); - element.append($compile(popUpEl)(scope)); + // required validator + if (attrs.required || attrs.ngRequired) { + required = true; + } + attrs.$observe('required', function (newVal) { + required = newVal; + }); - function getHeaderText() { - if (is_empty(modelCtrl.$modelValue)) return scope.header = attrs.msHeader || 'Select'; + // watch disabled state + scope.$watch(function () { + return $parse(attrs.disabled)(originalScope); + }, function (newVal) { + scope.disabled = newVal; + }); - if (isMultiple) { - if (attrs.msSelected) { - scope.header = $interpolate(attrs.msSelected)(scope); - } else { - if (modelCtrl.$modelValue.length == 1) { - for(var i = 0; i < scope.items.length; i++) { - if(scope.items[i].model === modelCtrl.$modelValue[0]) { - scope.header = scope.items[i].label; - } + // 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 parsedResult.source(originalScope); + }, function (newVal) { + if (angular.isDefined(newVal)) + parseModel(); + }, true); + + // watch model change + scope.$watch(function () { + return modelCtrl.$modelValue; + }, function (newVal, oldVal) { + // when directive initialize, newVal usually undefined. Also, if model value 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 + // model changes. We need to do this only if it is done outside directive scope, from controller, for example. + if (angular.isDefined(newVal)) { + markChecked(newVal); + scope.$eval(changeHandler); + } + getHeaderText(); + modelCtrl.$setValidity('required', scope.valid()); + }, true); + + function parseModel() { + scope.items.length = 0; + var model = parsedResult.source(originalScope); + if (!angular.isDefined(model)) return; + for (var i = 0; i < model.length; i++) { + var local = {}; + local[parsedResult.itemName] = model[i]; + var item = { + label: parsedResult.viewMapper(local), + model: parsedResult.modelMapper(local), + checked: false + }; + if (scope.id) + item.id = item.model[scope.id]; + + scope.items.push(item); } - } else { - scope.header = modelCtrl.$modelValue.length + ' ' + 'selected'; } - } - } else { - if(angular.isString(modelCtrl.$modelValue)){ - scope.header = modelCtrl.$modelValue; - } else { - var local = {}; - local[parsedResult.itemName] = modelCtrl.$modelValue; - scope.header = parsedResult.viewMapper(local) || scope.items[modelCtrl.$modelValue].label; - } - } - } - function is_empty(obj) { - if (angular.isNumber(obj)) return false; - if (obj && obj.length && obj.length > 0) return false; - for (var prop in obj) if (obj[prop]) return false; - return true; - }; + parseModel(); + + element.append($compile(popUpEl)(scope)); + + function getHeaderText() { + if (is_empty(modelCtrl.$modelValue)) return scope.header = attrs.msHeader || 'Select'; + + if (isMultiple) { + if (attrs.msSelected) { + scope.header = $interpolate(attrs.msSelected)(scope); + } else { + if (modelCtrl.$modelValue.length == 1) { + for (var i = 0; i < scope.items.length; i++) { + if (scope.items[i].model === modelCtrl.$modelValue[0]) { + scope.header = scope.items[i].label; + } + } + } else { + scope.header = modelCtrl.$modelValue.length + ' ' + 'selected'; + } + } + } else { + if (angular.isString(modelCtrl.$modelValue)) { + scope.header = modelCtrl.$modelValue; + } else { + var local = {}; + local[parsedResult.itemName] = modelCtrl.$modelValue; + scope.header = parsedResult.viewMapper(local) || scope.items[modelCtrl.$modelValue].label; + } + } + } - scope.valid = function validModel() { - if(!required) return true; - var value = modelCtrl.$modelValue; - return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value != null); - }; + function is_empty(obj) { + if (angular.isNumber(obj)) return false; + if (obj && obj.length && obj.length > 0) return false; + for (var prop in obj) if (obj[prop]) return false; + return true; + }; + + scope.valid = function validModel() { + if (!required) return true; + var value = modelCtrl.$modelValue; + return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value != null); + }; + + function selectSingle(item) { + if (item.checked) { + scope.uncheckAll(); + } else { + scope.uncheckAll(); + item.checked = !item.checked; + } + setModelValue(false); + invokeOnChange(item); + } - function selectSingle(item) { - if (item.checked) { - scope.uncheckAll(); - } else { - scope.uncheckAll(); - item.checked = !item.checked; - } - setModelValue(false); - } + function invokeOnChange(item) { + scope.$eval(onChange, { + event: { + data: item.model, + action: item.checked ? 'selected' : 'unselected' + } + }); + } - function selectMultiple(item) { - item.checked = !item.checked; - setModelValue(true); - } + function selectMultiple(item) { + item.checked = !item.checked; + setModelValue(true); + invokeOnChange(item); + } - function setModelValue(isMultiple) { - var value = null; - - if (isMultiple) { - value = []; - angular.forEach(scope.items, function (item) { - if (item.checked) value.push(item.model); - }) - } else { - angular.forEach(scope.items, function (item) { - if (item.checked) { - value = item.model; - return false; + function setModelValue(isMultiple) { + var value = null; + + if (isMultiple) { + value = []; + angular.forEach(scope.items, function (item) { + if (item.checked) value.push(item.model); + }) + } else { + angular.forEach(scope.items, function (item) { + if (item.checked) { + value = item.model; + return false; + } + }) + } + modelCtrl.$setViewValue(value); } - }) - } - modelCtrl.$setViewValue(value); - } - function markChecked(newVal) { - if (!angular.isArray(newVal)) { - angular.forEach(scope.items, function (item) { - if (angular.equals(item.model, newVal)) { - scope.uncheckAll(); - item.checked = true; - setModelValue(false); - return false; + function markChecked(newVal) { + if (!angular.isArray(newVal)) { + angular.forEach(scope.items, function (item) { + if (angular.equals(item.model, newVal)) { + scope.uncheckAll(); + item.checked = true; + setModelValue(false); + return false; + } + }); + } else { + angular.forEach(scope.items, function (item) { + item.checked = false; + angular.forEach(newVal, function (i) { + if (angular.equals(item.model, i)) { + item.checked = true; + } + }); + }); + } } - }); - } else { - angular.forEach(scope.items, function (item) { - item.checked = false; - angular.forEach(newVal, function (i) { - if (angular.equals(item.model, i)) { + + scope.checkAll = function () { + if (!isMultiple) return; + angular.forEach(scope.items, function (item) { item.checked = true; + }); + setModelValue(true); + }; + + scope.uncheckAll = function () { + angular.forEach(scope.items, function (item) { + item.checked = false; + }); + setModelValue(true); + }; + + scope.select = function (item) { + if (isMultiple === false) { + selectSingle(item); + scope.toggleSelect(); + } else { + selectMultiple(item); } - }); - }); - } - } - - scope.checkAll = function () { - if (!isMultiple) return; - angular.forEach(scope.items, function (item) { - item.checked = true; - }); - setModelValue(true); - }; - - scope.uncheckAll = function () { - angular.forEach(scope.items, function (item) { - item.checked = false; - }); - setModelValue(true); - }; - - scope.select = function (item) { - if (isMultiple === false) { - selectSingle(item); - scope.toggleSelect(); - } else { - selectMultiple(item); - } - } - } - }; -}]) - -.directive('amMultiselectPopup', ['$document', '$filter', function ($document, $filter) { - return { - restrict: 'E', - scope: false, - replace: true, - templateUrl: function (element, attr) { - return attr.templateUrl || 'multiselect.tmpl.html'; - }, - link: function (scope, element, attrs) { - - scope.selectedIndex = null; - scope.isVisible = false; - - scope.toggleSelect = function () { - if (element.hasClass('open')) { - element.removeClass('open'); - $document.unbind('click', clickHandler); - scope.$parent.$eval(scope.onBlur); - } else { - element.addClass('open'); - $document.bind('click', clickHandler); - scope.focus(); + } } }; + }]) + + .directive('amMultiselectPopup', ['$document', '$filter', function ($document, $filter) { + return { + restrict: 'E', + scope: false, + replace: true, + templateUrl: function (element, attr) { + return attr.templateUrl || 'multiselect.tmpl.html'; + }, + link: function (scope, element, attrs) { + + scope.selectedIndex = null; + scope.isVisible = false; + + scope.toggleSelect = function () { + if (element.hasClass('open')) { + element.removeClass('open'); + $document.unbind('click', clickHandler); + scope.$parent.$eval(scope.onBlur); + } else { + element.addClass('open'); + $document.bind('click', clickHandler); + scope.focus(); + } + }; - function clickHandler(event) { - if (elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) { - scope.$parent.$eval(scope.onBlur); - } else { - element.removeClass('open'); - $document.unbind('click', clickHandler); - scope.$apply(); + function clickHandler(event) { + if (elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) { + scope.$parent.$eval(scope.onBlur); + } else { + element.removeClass('open'); + $document.unbind('click', clickHandler); + scope.$apply(); + } } - } - scope.focus = function focus(){ - var searchBox = element.find('input')[0]; - if (searchBox) { - searchBox.focus(); + scope.focus = function focus() { + var searchBox = element.find('input')[0]; + if (searchBox) { + searchBox.focus(); + } } - } - scope.keydown = function (event) { - var list = $filter('filter')(scope.items, scope.searchText), - keyCode = (event.keyCode || event.which); + scope.keydown = function (event) { + var list = $filter('filter')(scope.items, scope.searchText), + keyCode = (event.keyCode || event.which); - if(keyCode === 13){ // On enter - if(list[scope.selectedIndex]){ - scope.select(list[scope.selectedIndex]); // (un)select item + if (keyCode === 13) { // On enter + if (list[scope.selectedIndex]) { + scope.select(list[scope.selectedIndex]); // (un)select item + } + } else if (keyCode === 38) { // On arrow up + scope.selectedIndex = scope.selectedIndex === null ? list.length - 1 : scope.selectedIndex - 1; + } else if (keyCode === 40) { // On arrow down + scope.selectedIndex = scope.selectedIndex === null ? 0 : scope.selectedIndex + 1; + } else { // On any other key + scope.selectedIndex = null; } - }else if(keyCode === 38){ // On arrow up - scope.selectedIndex = scope.selectedIndex===null ? list.length-1 : scope.selectedIndex-1; - }else if(keyCode === 40){ // On arrow down - scope.selectedIndex = scope.selectedIndex===null ? 0 : scope.selectedIndex+1; - }else{ // On any other key - scope.selectedIndex = null; - } - if(scope.selectedIndex < 0){ // Select last in list - scope.selectedIndex = list.length-1; - }else if(scope.selectedIndex > list.length-1){ // Set selection to first item in list - scope.selectedIndex = 0; - } - }; + if (scope.selectedIndex < 0) { // Select last in list + scope.selectedIndex = list.length - 1; + } else if (scope.selectedIndex > list.length - 1) { // Set selection to first item in list + scope.selectedIndex = 0; + } + }; - var elementMatchesAnyInArray = function (element, elementArray) { - for (var i = 0; i < elementArray.length; i++) - if (element == elementArray[i]) - return true; - return false; + var elementMatchesAnyInArray = function (element, elementArray) { + for (var i = 0; i < elementArray.length; i++) + if (element == elementArray[i]) + return true; + return false; + } } } - } -}]); + }]); diff --git a/dist/multiselect.tmpl.html b/dist/multiselect.tmpl.html index 4a57429..d934282 100644 --- a/dist/multiselect.tmpl.html +++ b/dist/multiselect.tmpl.html @@ -3,7 +3,7 @@ {{header}} -